mission_control-jobs 0.6.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +39 -4
  3. data/app/controllers/concerns/mission_control/jobs/basic_authentication.rb +33 -0
  4. data/app/controllers/concerns/mission_control/jobs/job_filters.rb +16 -1
  5. data/app/controllers/mission_control/jobs/application_controller.rb +12 -0
  6. data/app/helpers/mission_control/jobs/dates_helper.rb +16 -2
  7. data/app/helpers/mission_control/jobs/jobs_helper.rb +1 -1
  8. data/app/views/mission_control/jobs/jobs/_filters.html.erb +11 -1
  9. data/app/views/mission_control/jobs/jobs/_general_information.html.erb +4 -4
  10. data/app/views/mission_control/jobs/jobs/_job.html.erb +1 -1
  11. data/app/views/mission_control/jobs/jobs/blocked/_job.html.erb +1 -1
  12. data/app/views/mission_control/jobs/jobs/failed/_job.html.erb +1 -1
  13. data/app/views/mission_control/jobs/jobs/finished/_job.html.erb +1 -1
  14. data/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb +1 -1
  15. data/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb +1 -1
  16. data/app/views/mission_control/jobs/queues/_job.html.erb +1 -1
  17. data/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb +2 -2
  18. data/app/views/mission_control/jobs/shared/_job.html.erb +3 -3
  19. data/app/views/mission_control/jobs/workers/_worker.html.erb +1 -1
  20. data/lib/active_job/jobs_relation.rb +7 -5
  21. data/lib/active_job/queue_adapters/solid_queue_ext.rb +7 -2
  22. data/lib/mission_control/jobs/authentication.rb +67 -0
  23. data/lib/mission_control/jobs/engine.rb +11 -2
  24. data/lib/mission_control/jobs/i18n_config.rb +5 -0
  25. data/lib/mission_control/jobs/tasks.rb +8 -0
  26. data/lib/mission_control/jobs/version.rb +1 -1
  27. data/lib/mission_control/jobs.rb +13 -3
  28. metadata +11 -4
  29. data/lib/tasks/mission_control/jobs_tasks.rake +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90d1d2bb94c81934c6d0f2c366b76ba9d9ee778c90a4ca0385515ea7b84d658f
4
- data.tar.gz: 51f7c76c6de7a39cc18e5b813a7d84ae18c94e9cd25487c0f16cb94e650d2319
3
+ metadata.gz: 8afd612ead034dc5c0f4b3798f62943bf021a557e4af5d7a5a565e50bfe5d647
4
+ data.tar.gz: 077c09bbff77e7f73de506600c447a05f7cb08b8ee266a1b107395ac393338e9
5
5
  SHA512:
6
- metadata.gz: 69e67bae39f22105acac0e10b9c52452fb453acc6e52e868746875f9daa75cd5556eadfca06db76eecb70e6c634dfdc1ecc217cc001d726da9b93bd9606b7ebe
7
- data.tar.gz: ab9c390dfd70c8b196ee3e6c1e5c07821299c2b23fd733a62c39d7b01cab3350f53c17ae9c9bdf5aaad9becf8b50b55dfd34b555b30500585373f901296e7717
6
+ metadata.gz: 99c16735667988a38b3b9f356406f8aec967044cf17f0f81cc05f8b6bb5fb2a85965ad34c30cf537ad95c43935cd0cc6e5f3ec7185ec661f06ec3579a8e73ab2
7
+ data.tar.gz: af8726be9391650a8f27c0ba855ecd633dd5bb23d9d0be78a8d5d5f4fdb9949d8d65f95ee638aebcede5fa863fa20c76ea054b8e7a3d7021bd0408ec0877c26e
data/README.md CHANGED
@@ -32,7 +32,7 @@ And that's it. With this alone, you should be able to access Mission Control Job
32
32
 
33
33
  ### API-only apps or apps using `vite_rails` and other asset pipelines outside Rails
34
34
 
35
- If you want to use this gem with an [API-only Rails app](https://guides.rubyonrails.org/api_app.html) or an app that's using `vite_ruby`/`vite_rails`, or some other custom asset pipeline different from Sprockets and Propshaft, you need just one more thing: configure an asset pipeline so you can serve the JavaScript and CSS included in this gem. We recommend to use [`Propshaft`](https://github.com/rails/propshaft). You simply need to add this line to your application's Gemfile:
35
+ If you want to use this gem with an [API-only Rails app](https://guides.rubyonrails.org/api_app.html) or an app that's using `vite_ruby`/`vite_rails`, or some other custom asset pipeline different from Sprockets and Propshaft, you need one more thing: configure an asset pipeline so you can serve the JavaScript and CSS included in this gem. We recommend to use [`Propshaft`](https://github.com/rails/propshaft). You simply need to add this line to your application's Gemfile:
36
36
 
37
37
  ```ruby
38
38
  gem "propshaft"
@@ -43,11 +43,42 @@ Then execute
43
43
  $ bundle install
44
44
  ```
45
45
 
46
- And you should be ready to go.
46
+ Then, make sure you add a step to your deployment pipeline to precompile assets:
47
+ ```
48
+ RAILS_ENV=production rails assets:precompile
49
+ ```
50
+
51
+ For example, if you're using the Dockerfile generated by Rails with an API-only app or having skipped the assets pipeline, re-add:
52
+ ```
53
+ # Precompiling assets for production without requiring secret RAILS_MASTER_KEY
54
+ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
55
+ ```
56
+
57
+ *Note: Legacy CSS bundlers `sass-rails` and `sassc-rails` may fail to compile some of the CSS vendored into this library from [Bulma](https://github.com/jgthms/bulma), which was created in [Dart SASS](https://sass-lang.com/dart-sass/). You will therefore need to upgrade to `dartsass-rails` or some library that relies on it, like `cssbundling-rails`.*
58
+
59
+ ### Authentication
60
+
61
+ Mission Control comes with **HTTP basic authentication enabled and closed** by default. Credentials are stored in [Rails's credentials](https://edgeguides.rubyonrails.org/security.html#custom-credentials) like this:
62
+ ```yml
63
+ mission_control:
64
+ http_basic_auth_user: dev
65
+ http_basic_auth_password: secret
66
+ ```
47
67
 
48
- ### Authentication and base controller class
68
+ If no credentials are configured, Mission Control won't be accessible. To set these up, you can run the generator provided like this:
49
69
 
50
- By default, Mission Control's controllers will extend the host app's `ApplicationController`. If no authentication is enforced, `/jobs` will be available to everyone. You might want to implement some kind of authentication for this in your app. To make this easier, you can specify a different controller as the base class for Mission Control's controllers:
70
+ ```
71
+ bin/rails mission_control:jobs:authentication:configure
72
+ ```
73
+
74
+ To set them up for different environments you can use the `RAILS_ENV` environment variable, like this:
75
+ ```
76
+ RAILS_ENV=production bin/rails mission_control:jobs:authentication:configure
77
+ ```
78
+
79
+ #### Custom authentication
80
+
81
+ You can provide your own authentication mechanism, for example, if you have a certain type of admin user in your app that can access Mission Control. To make this easier, you can specify a different controller as the base class for Mission Control's controllers. By default, Mission Control's controllers will extend the host app's `ApplicationController`, but you can change this easily:
51
82
 
52
83
  ```ruby
53
84
  Rails.application.configure do
@@ -58,7 +89,11 @@ end
58
89
  Or, in your environment config or `application.rb`:
59
90
  ```ruby
60
91
  config.mission_control.jobs.base_controller_class = "AdminController"
92
+ ```
61
93
 
94
+ If you do this, you can disable the default HTTP Basic Authentication using the following option:
95
+ ```ruby
96
+ config.mission_control.jobs.http_basic_auth_enabled = false
62
97
  ```
63
98
 
64
99
  ### Other configuration settings
@@ -0,0 +1,33 @@
1
+ module MissionControl::Jobs::BasicAuthentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :authenticate_by_http_basic
6
+ end
7
+
8
+ private
9
+ def authenticate_by_http_basic
10
+ if http_basic_authentication_enabled?
11
+ if http_basic_authentication_configured?
12
+ http_basic_authenticate_or_request_with(**http_basic_authentication_credentials)
13
+ else
14
+ head :unauthorized
15
+ end
16
+ end
17
+ end
18
+
19
+ def http_basic_authentication_enabled?
20
+ MissionControl::Jobs.http_basic_auth_enabled
21
+ end
22
+
23
+ def http_basic_authentication_configured?
24
+ http_basic_authentication_credentials.values.all?(&:present?)
25
+ end
26
+
27
+ def http_basic_authentication_credentials
28
+ {
29
+ name: MissionControl::Jobs.http_basic_auth_user,
30
+ password: MissionControl::Jobs.http_basic_auth_password
31
+ }.transform_values(&:presence)
32
+ end
33
+ end
@@ -9,10 +9,25 @@ module MissionControl::Jobs::JobFilters
9
9
 
10
10
  private
11
11
  def set_filters
12
- @job_filters = { job_class_name: params.dig(:filter, :job_class_name).presence, queue_name: params.dig(:filter, :queue_name).presence }.compact
12
+ @job_filters = {
13
+ job_class_name: params.dig(:filter, :job_class_name).presence,
14
+ queue_name: params.dig(:filter, :queue_name).presence,
15
+ finished_at: finished_at_range_params
16
+ }.compact
13
17
  end
14
18
 
15
19
  def active_filters?
16
20
  @job_filters.any?
17
21
  end
22
+
23
+ def finished_at_range_params
24
+ range_start, range_end = params.dig(:filter, :finished_at_start), params.dig(:filter, :finished_at_end)
25
+ if range_start || range_end
26
+ (parse_with_time_zone(range_start)..parse_with_time_zone(range_end))
27
+ end
28
+ end
29
+
30
+ def parse_with_time_zone(date)
31
+ DateTime.parse(date).in_time_zone if date.present?
32
+ end
18
33
  end
@@ -9,11 +9,23 @@ class MissionControl::Jobs::ApplicationController < MissionControl::Jobs.base_co
9
9
  helper MissionControl::Jobs::ApplicationHelper unless self < MissionControl::Jobs::ApplicationHelper
10
10
  helper Importmap::ImportmapTagsHelper unless self < Importmap::ImportmapTagsHelper
11
11
 
12
+ include MissionControl::Jobs::BasicAuthentication
12
13
  include MissionControl::Jobs::ApplicationScoped, MissionControl::Jobs::NotFoundRedirections
13
14
  include MissionControl::Jobs::AdapterFeatures
14
15
 
16
+ around_action :set_current_locale
17
+
15
18
  private
16
19
  def default_url_options
17
20
  { server_id: MissionControl::Jobs::Current.server }
18
21
  end
22
+
23
+ def set_current_locale(&block)
24
+ @previous_config = I18n.config
25
+ I18n.config = MissionControl::Jobs::I18nConfig.new
26
+ I18n.with_locale(:en, &block)
27
+ ensure
28
+ I18n.config = @previous_config
29
+ @previous_config = nil
30
+ end
19
31
  end
@@ -1,5 +1,19 @@
1
1
  module MissionControl::Jobs::DatesHelper
2
- def formatted_time(time)
3
- time.in_time_zone.strftime("%Y-%m-%d %H:%M:%S.%3N %Z")
2
+ def time_distance_in_words_with_title(time)
3
+ tag.span time_ago_in_words_with_default_options(time), title: "Since #{time.to_fs(:long)}"
4
+ end
5
+
6
+ def bidirectional_time_distance_in_words_with_title(time)
7
+ time_distance = if time.past?
8
+ "#{time_ago_in_words_with_default_options(time)} ago"
9
+ else
10
+ "in #{time_ago_in_words_with_default_options(time)}"
11
+ end
12
+
13
+ tag.span time_distance, title: time.to_fs(:long)
14
+ end
15
+
16
+ def time_ago_in_words_with_default_options(time)
17
+ time_ago_in_words(time, include_seconds: true, locale: :en)
4
18
  end
5
19
  end
@@ -29,7 +29,7 @@ module MissionControl::Jobs::JobsHelper
29
29
  when "blocked" then [ "Queue", "Blocked by", "" ]
30
30
  when "finished" then [ "Queue", "Finished" ]
31
31
  when "scheduled" then [ "Queue", "Scheduled", "" ]
32
- when "in_progress" then [ "Queue", "Run by", "Running since" ]
32
+ when "in_progress" then [ "Queue", "Run by", "Running for" ]
33
33
  else []
34
34
  end
35
35
  end
@@ -12,6 +12,16 @@
12
12
  <%= form.text_field :queue_name, value: @job_filters[:queue_name], class: "input", list: "queue-names", placeholder: "Filter by queue name..." %>
13
13
  </div>
14
14
 
15
+ <% if jobs_status == "finished" %>
16
+ <div class="select is-rounded">
17
+ <%= form.datetime_field :finished_at_start, value: @job_filters[:finished_at]&.begin, class: "input", placeholder: "Finished from" %>
18
+ </div>
19
+
20
+ <div class="select is-rounded">
21
+ <%= form.datetime_field :finished_at_end, value: @job_filters[:finished_at]&.end, class: "input", placeholder: "Finished to" %>
22
+ </div>
23
+ <% end %>
24
+
15
25
  <%= hidden_field_tag :server_id, MissionControl::Jobs::Current.server.id %>
16
26
 
17
27
  <datalist id="job-classes" class="is-hidden">
@@ -29,7 +39,7 @@
29
39
  </div>
30
40
 
31
41
  <div class="control">
32
- <%= link_to "Clear", application_jobs_path(MissionControl::Jobs::Current.application, jobs_status, job_class_name: nil, queue_name: nil), class: "button" %>
42
+ <%= 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" %>
33
43
  </div>
34
44
  </div>
35
45
  </div>
@@ -23,14 +23,14 @@
23
23
  <tr>
24
24
  <th>Enqueued</th>
25
25
  <td>
26
- <%= formatted_time(job.enqueued_at.to_datetime) %>
26
+ <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
27
27
  </td>
28
28
  </tr>
29
29
  <% if job.scheduled? %>
30
30
  <tr>
31
31
  <th>Scheduled</th>
32
32
  <td>
33
- <%= formatted_time(job.scheduled_at) %>
33
+ <%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %>
34
34
  <% if job_delayed?(job) %>
35
35
  <div class="is-danger tag ml-4">delayed</div>
36
36
  <% end %>
@@ -41,7 +41,7 @@
41
41
  <tr>
42
42
  <th>Failed</th>
43
43
  <td>
44
- <%= formatted_time(job.failed_at) %>
44
+ <%= time_distance_in_words_with_title(job.failed_at) %> ago
45
45
  </td>
46
46
  </tr>
47
47
  <% end %>
@@ -49,7 +49,7 @@
49
49
  <tr>
50
50
  <th>Finished at</th>
51
51
  <td>
52
- <%= formatted_time(job.finished_at) %>
52
+ <%= time_distance_in_words_with_title(job.finished_at) %> ago
53
53
  </td>
54
54
  </tr>
55
55
  <% end %>
@@ -6,7 +6,7 @@
6
6
  <div class="is-family-monospace"><%= job_arguments(job) %></div>
7
7
  <% end %>
8
8
 
9
- <div class="has-text-grey is-size-7">Enqueued <%= formatted_time(job.enqueued_at.to_datetime) %></div>
9
+ <div class="has-text-grey is-size-7">Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago</div>
10
10
  </td>
11
11
 
12
12
  <%= render "mission_control/jobs/jobs/#{jobs_status}/job", job: job %>
@@ -1,6 +1,6 @@
1
1
  <td><%= link_to job.queue_name, application_queue_path(@application, job.queue) %></td>
2
2
  <td><div class="is-family-monospace is-size-7"><%= job.blocked_by %></div>
3
- <div class="has-text-grey is-size-7">Until <%= formatted_time(job.blocked_until) %></div>
3
+ <div class="has-text-grey is-size-7">Expires <%= bidirectional_time_distance_in_words_with_title(job.blocked_until) %></div>
4
4
  </td>
5
5
  <td class="pr-0">
6
6
  <%= render "mission_control/jobs/jobs/blocked/actions", job: job %>
@@ -1,6 +1,6 @@
1
1
  <td>
2
2
  <%= link_to failed_job_error(job), application_job_path(@application, job.job_id, anchor: "error") %>
3
- <div class="has-text-grey"><%= formatted_time(job.failed_at) %></div>
3
+ <div class="has-text-grey"><%= time_distance_in_words_with_title(job.failed_at) %> ago</div>
4
4
  </td>
5
5
  <td class="pr-0">
6
6
  <%= render "mission_control/jobs/jobs/failed/actions", job: job %>
@@ -1,2 +1,2 @@
1
1
  <td><%= link_to job.queue_name, application_queue_path(@application, job.queue) %></td>
2
- <td><div class="has-text-grey"><%= formatted_time(job.finished_at) %></div></td>
2
+ <td><div class="has-text-grey"><%= time_distance_in_words_with_title(job.finished_at) %> ago</div></td>
@@ -6,4 +6,4 @@
6
6
 
7
7
  <% end %>
8
8
  </td>
9
- <td><div class="has-text-grey"><%= job.started_at ? formatted_time(job.started_at) : "(Finished)" %></div></td>
9
+ <td><div class="has-text-grey"><%= job.started_at ? time_distance_in_words_with_title(job.started_at) : "(Finished)" %></div></td>
@@ -1,6 +1,6 @@
1
1
  <td><%= link_to job.queue_name, application_queue_path(@application, job.queue) %></td>
2
2
  <td>
3
- <%= formatted_time(job.scheduled_at) %>
3
+ <%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %>
4
4
  <% if job_delayed?(job) %>
5
5
  <div class="is-danger tag ml-4">delayed</div>
6
6
  <% end %>
@@ -3,7 +3,7 @@
3
3
  <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %>
4
4
  <%= job_title(job) %>
5
5
  <% end %>
6
- <div class="has-text-grey">Enqueued on <%= formatted_time(job.enqueued_at.to_datetime) %></div>
6
+ <div class="has-text-grey">Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago</div>
7
7
  </td>
8
8
  <td>
9
9
  <% if job.serialized_arguments.present? %>
@@ -14,8 +14,8 @@
14
14
  <% end %>
15
15
  </td>
16
16
  <td> <%= recurring_task.schedule %> </td>
17
- <td><div class="has-text-grey"><%= recurring_task.last_enqueued_at ? formatted_time(recurring_task.last_enqueued_at) : "Never" %></div></td>
18
- <td class="next_time"><div class="has-text-grey"><%= formatted_time(recurring_task.next_time) %></div></td>
17
+ <td><div class="has-text-grey"><%= recurring_task.last_enqueued_at ? bidirectional_time_distance_in_words_with_title(recurring_task.last_enqueued_at) : "Never" %></div></td>
18
+ <td class="next_time"><div class="has-text-grey"><%= bidirectional_time_distance_in_words_with_title(recurring_task.next_time) %></div></td>
19
19
  <td class="pr-0">
20
20
  <%= render "mission_control/jobs/recurring_tasks/actions", recurring_task: recurring_task %>
21
21
  </td>
@@ -3,7 +3,7 @@
3
3
  <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %>
4
4
  <%= job_title(job) %>
5
5
  <% end %>
6
- <div class="has-text-grey">Enqueued on <%= formatted_time(job.enqueued_at.to_datetime) %></div>
6
+ <div class="has-text-grey">Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago</div>
7
7
  </td>
8
8
  <td>
9
9
  <% if job.serialized_arguments.present? %>
@@ -16,9 +16,9 @@
16
16
  <td>
17
17
  <div class="has-text-grey">
18
18
  <% if job.started_at %>
19
- Running since <%= formatted_time(job.started_at) %>
19
+ Running for <%= time_distance_in_words_with_title(job.started_at) %>
20
20
  <% elsif job.finished_at %>
21
- Finished on <%= formatted_time(job.finished_at) %>
21
+ Finished <%= time_distance_in_words_with_title(job.finished_at) %> ago
22
22
  <% else %>
23
23
  Pending
24
24
  <% end %>
@@ -17,5 +17,5 @@
17
17
  <% end %>
18
18
  </td>
19
19
 
20
- <td><div class="has-text-grey"><%= formatted_time(worker.last_heartbeat_at) %></div></td>
20
+ <td><div class="has-text-grey"><%= time_distance_in_words_with_title(worker.last_heartbeat_at) %> ago</div></td>
21
21
  </tr>
@@ -25,7 +25,7 @@ class ActiveJob::JobsRelation
25
25
  STATUSES = %i[ pending failed in_progress blocked scheduled finished ]
26
26
  FILTERS = %i[ queue_name job_class_name ]
27
27
 
28
- PROPERTIES = %i[ queue_name status offset_value limit_value job_class_name worker_id recurring_task_id ]
28
+ PROPERTIES = %i[ queue_name status offset_value limit_value job_class_name worker_id recurring_task_id finished_at ]
29
29
  attr_reader *PROPERTIES, :default_page_size
30
30
 
31
31
  delegate :last, :[], :reverse, to: :to_a
@@ -51,13 +51,15 @@ class ActiveJob::JobsRelation
51
51
  # * <tt>:queue_name</tt> - To only include the jobs in the provided queue.
52
52
  # * <tt>:worker_id</tt> - To only include the jobs processed by the provided worker.
53
53
  # * <tt>:recurring_task_id</tt> - To only include the jobs corresponding to runs of a recurring task.
54
- def where(job_class_name: nil, queue_name: nil, worker_id: nil, recurring_task_id: nil)
54
+ # * <tt>:finished_at</tt> - (Range) To only include the jobs finished between the provided range
55
+ def where(job_class_name: nil, queue_name: nil, worker_id: nil, recurring_task_id: nil, finished_at: nil)
55
56
  # Remove nil arguments to avoid overriding parameters when concatenating +where+ clauses
56
57
  arguments = { job_class_name: job_class_name,
57
- queue_name: queue_name,
58
+ queue_name: queue_name&.to_s,
58
59
  worker_id: worker_id,
59
- recurring_task_id: recurring_task_id
60
- }.compact.collect { |key, value| [ key, value.to_s ] }.to_h
60
+ recurring_task_id: recurring_task_id,
61
+ finished_at: finished_at
62
+ }.compact
61
63
 
62
64
  clone_with **arguments
63
65
  end
@@ -40,7 +40,7 @@ module ActiveJob::QueueAdapters::SolidQueueExt
40
40
  end
41
41
 
42
42
  def supported_job_filters(*)
43
- [ :queue_name, :job_class_name ]
43
+ [ :queue_name, :job_class_name, :finished_at ]
44
44
  end
45
45
 
46
46
  def jobs_count(jobs_relation)
@@ -173,7 +173,7 @@ module ActiveJob::QueueAdapters::SolidQueueExt
173
173
  attr_reader :jobs_relation
174
174
 
175
175
  delegate :queue_name, :limit_value, :limit_value_provided?, :offset_value, :job_class_name,
176
- :default_page_size, :worker_id, :recurring_task_id, to: :jobs_relation
176
+ :default_page_size, :worker_id, :recurring_task_id, :finished_at, to: :jobs_relation
177
177
 
178
178
  def executions
179
179
  execution_class_by_status
@@ -190,6 +190,7 @@ module ActiveJob::QueueAdapters::SolidQueueExt
190
190
  SolidQueue::Job.finished
191
191
  .then { |jobs| filter_jobs_by_queue(jobs) }
192
192
  .then { |jobs| filter_jobs_by_class(jobs) }
193
+ .then { |jobs| filter_jobs_by_finished_at(jobs) }
193
194
  .then { |jobs| limit(jobs) }
194
195
  .then { |jobs| offset(jobs) }
195
196
  end
@@ -271,6 +272,10 @@ module ActiveJob::QueueAdapters::SolidQueueExt
271
272
  job_class_name.present? ? jobs.where(class_name: job_class_name) : jobs
272
273
  end
273
274
 
275
+ def filter_jobs_by_finished_at(jobs)
276
+ finished_at.present? ? jobs.where(finished_at: finished_at) : jobs
277
+ end
278
+
274
279
  def limit(executions_or_jobs)
275
280
  limit_value.present? ? executions_or_jobs.limit(limit_value) : executions_or_jobs
276
281
  end
@@ -0,0 +1,67 @@
1
+ require "rails/command"
2
+
3
+ class MissionControl::Jobs::Authentication < Rails::Command::Base
4
+ def self.configure
5
+ new.configure
6
+ end
7
+
8
+ def configure
9
+ if credentials_accessible?
10
+ if authentication_configured?
11
+ say "HTTP Basic Authentication is already configured for `#{Rails.env}`. You can edit it using `credentials:edit`"
12
+ else
13
+ say "Setting up credentials for HTTP Basic Authentication for `#{Rails.env}` environment."
14
+ say ""
15
+
16
+ username = ask "Enter username: "
17
+ password = SecureRandom.base58(64)
18
+
19
+ store_credentials(username, password)
20
+ say "Username and password stored in Rails encrypted credentials."
21
+ say ""
22
+ say "You can now access Mission Control – Jobs with: "
23
+ say ""
24
+ say " - Username: #{username}"
25
+ say " - password: #{password}"
26
+ say ""
27
+ say "You can also edit these in the future via `credentials:edit`"
28
+ end
29
+ else
30
+ say "Rails credentials haven't been configured or aren't accessible. Configure them following the instructions in `credentials:help`"
31
+ end
32
+ end
33
+
34
+ private
35
+ attr_reader :environment
36
+
37
+ def credentials_accessible?
38
+ credentials.read.present?
39
+ end
40
+
41
+ def authentication_configured?
42
+ %i[ http_basic_auth_user http_basic_auth_password ].any? do |key|
43
+ credentials.dig(:mission_control, key).present?
44
+ end
45
+ end
46
+
47
+ def store_credentials(username, password)
48
+ content = credentials.read + "\n" + http_authentication_entry(username, password) + "\n"
49
+ credentials.write(content)
50
+ end
51
+
52
+ def credentials
53
+ @credentials ||= Rails.application.encrypted(config.content_path, key_path: config.key_path)
54
+ end
55
+
56
+ def config
57
+ Rails.application.config.credentials
58
+ end
59
+
60
+ def http_authentication_entry(username, password)
61
+ <<~ENTRY
62
+ mission_control:
63
+ http_basic_auth_user: #{username}
64
+ http_basic_auth_password: #{password}
65
+ ENTRY
66
+ end
67
+ end
@@ -7,10 +7,14 @@ module MissionControl
7
7
  class Engine < ::Rails::Engine
8
8
  isolate_namespace MissionControl::Jobs
9
9
 
10
+ rake_tasks do
11
+ load "mission_control/jobs/tasks.rb"
12
+ end
13
+
10
14
  initializer "mission_control-jobs.middleware" do |app|
11
15
  if app.config.api_only
12
- app.middleware.use ActionDispatch::Flash
13
- app.middleware.use ::Rack::MethodOverride
16
+ config.middleware.use ActionDispatch::Flash
17
+ config.middleware.use ::Rack::MethodOverride
14
18
  end
15
19
  end
16
20
 
@@ -30,6 +34,11 @@ module MissionControl
30
34
  end
31
35
  end
32
36
 
37
+ initializer "mission_control-jobs.http_basic_auth" do |app|
38
+ MissionControl::Jobs.http_basic_auth_user = app.credentials.dig(:mission_control, :http_basic_auth_user)
39
+ MissionControl::Jobs.http_basic_auth_password = app.credentials.dig(:mission_control, :http_basic_auth_password)
40
+ end
41
+
33
42
  initializer "mission_control-jobs.active_job.extensions" do
34
43
  ActiveSupport.on_load :active_job do
35
44
  include ActiveJob::Querying
@@ -0,0 +1,5 @@
1
+ class MissionControl::Jobs::I18nConfig < ::I18n::Config
2
+ def available_locales
3
+ [ :en ]
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ namespace :mission_control do
2
+ namespace :jobs do
3
+ desc "Configure HTTP Basic Authentication"
4
+ task "authentication:configure" => :environment do
5
+ MissionControl::Jobs::Authentication.configure
6
+ end
7
+ end
8
+ end
@@ -1,5 +1,5 @@
1
1
  module MissionControl
2
2
  module Jobs
3
- VERSION = "0.6.0"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  end
@@ -7,6 +7,8 @@ loader = Zeitwerk::Loader.new
7
7
  loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
8
8
  loader.push_dir(File.expand_path("..", __dir__))
9
9
  loader.ignore("#{File.expand_path("..", __dir__)}/resque")
10
+ loader.ignore("#{File.expand_path("..", __dir__)}/mission_control/jobs/tasks.rb")
11
+ loader.ignore("#{File.expand_path("..", __dir__)}/generators")
10
12
  loader.setup
11
13
 
12
14
  module MissionControl
@@ -14,12 +16,20 @@ module MissionControl
14
16
  mattr_accessor :adapters, default: Set.new
15
17
  mattr_accessor :applications, default: MissionControl::Jobs::Applications.new
16
18
  mattr_accessor :base_controller_class, default: "::ApplicationController"
19
+
20
+ mattr_accessor :internal_query_count_limit, default: 500_000 # Hard limit to keep unlimited count queries fast enough
17
21
  mattr_accessor :delay_between_bulk_operation_batches, default: 0
22
+ mattr_accessor :scheduled_job_delay_threshold, default: 1.minute
23
+
18
24
  mattr_accessor :logger, default: ActiveSupport::Logger.new(nil)
19
- mattr_accessor :internal_query_count_limit, default: 500_000 # Hard limit to keep unlimited count queries fast enough
25
+
20
26
  mattr_accessor :show_console_help, default: true
21
- mattr_accessor :scheduled_job_delay_threshold, default: 1.minute
22
- mattr_accessor :importmap, default: Importmap::Map.new
23
27
  mattr_accessor :backtrace_cleaner
28
+
29
+ mattr_accessor :importmap, default: Importmap::Map.new
30
+
31
+ mattr_accessor :http_basic_auth_user
32
+ mattr_accessor :http_basic_auth_password
33
+ mattr_accessor :http_basic_auth_enabled, default: true
24
34
  end
25
35
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mission_control-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jorge Manrubia
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-15 00:00:00.000000000 Z
11
+ date: 2024-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -363,6 +363,7 @@ files:
363
363
  - app/assets/stylesheets/mission_control/jobs/jobs.css
364
364
  - app/controllers/concerns/mission_control/jobs/adapter_features.rb
365
365
  - app/controllers/concerns/mission_control/jobs/application_scoped.rb
366
+ - app/controllers/concerns/mission_control/jobs/basic_authentication.rb
366
367
  - app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb
367
368
  - app/controllers/concerns/mission_control/jobs/job_filters.rb
368
369
  - app/controllers/concerns/mission_control/jobs/job_scoped.rb
@@ -464,29 +465,35 @@ files:
464
465
  - lib/mission_control/jobs/adapter.rb
465
466
  - lib/mission_control/jobs/application.rb
466
467
  - lib/mission_control/jobs/applications.rb
468
+ - lib/mission_control/jobs/authentication.rb
467
469
  - lib/mission_control/jobs/console/connect_to.rb
468
470
  - lib/mission_control/jobs/console/context.rb
469
471
  - lib/mission_control/jobs/console/jobs_help.rb
470
472
  - lib/mission_control/jobs/engine.rb
471
473
  - lib/mission_control/jobs/errors/incompatible_adapter.rb
472
474
  - lib/mission_control/jobs/errors/resource_not_found.rb
475
+ - lib/mission_control/jobs/i18n_config.rb
473
476
  - lib/mission_control/jobs/identified_by_name.rb
474
477
  - lib/mission_control/jobs/identified_elements.rb
475
478
  - lib/mission_control/jobs/server.rb
476
479
  - lib/mission_control/jobs/server/recurring_tasks.rb
477
480
  - lib/mission_control/jobs/server/serializable.rb
478
481
  - lib/mission_control/jobs/server/workers.rb
482
+ - lib/mission_control/jobs/tasks.rb
479
483
  - lib/mission_control/jobs/version.rb
480
484
  - lib/mission_control/jobs/workers_relation.rb
481
485
  - lib/resque/thread_safe_redis.rb
482
- - lib/tasks/mission_control/jobs_tasks.rake
483
486
  homepage: https://github.com/rails/mission_control-jobs
484
487
  licenses:
485
488
  - MIT
486
489
  metadata:
487
490
  homepage_uri: https://github.com/rails/mission_control-jobs
488
491
  source_code_uri: https://github.com/rails/mission_control-jobs
489
- post_install_message:
492
+ post_install_message: |
493
+ Upgrading to Mission Control – Jobs 1.0.0? HTTP Basic authentication has been added by default, and it needs
494
+ to be configured or disabled before you can access the dashboard.
495
+ --> Check https://github.com/rails/mission_control-jobs?tab=readme-ov-file#authentication
496
+ for more details and instructions.
490
497
  rdoc_options: []
491
498
  require_paths:
492
499
  - lib
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :mission_control_jobs do
3
- # # Task goes here
4
- # end