maintenance_tasks 2.10.1 → 2.12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b660fdfcef27420bf9b08ddc50c9cfac5ff0977c4435b54ecd2b468a3e4fde8
4
- data.tar.gz: 30bac0da63839406dc623d07c85904b7a727d3af5c177736cbd72ea828ca41c9
3
+ metadata.gz: d307d92f96c1d1975cc3fbefcddf6ec2e11086097437b63594090c5bc524eea8
4
+ data.tar.gz: 9176d7a34c4a54f450601ea2748a88b1a9f134c9e416e97978e613bf3fbf1538
5
5
  SHA512:
6
- metadata.gz: 071dafa23a89bd949e40f10ef199ae892d5ae81ca2ad1e15e6c7de0260dafbd55f735d67c310f5de7ef499991982eb3bad69fce4024f8f8761e4357ac3fdc42b
7
- data.tar.gz: 77265626bdeafaa6486a3843846d519168be0712399ebe3cd7a4ecbb42f5888f208f566346d6d628643ee0dcf06ff1c4b914f2ab3ef9ec73f60b9444ef3f05c0
6
+ metadata.gz: 87d37b437e518c0398023335fe88cb0c7829387142873eeb01aa713df04f397e7711491f0ace1a32baae20f4f1a613313c59e95696deef73d6ecfad24b6fb267
7
+ data.tar.gz: b0c5fce124651391b77b8f2e5d91f8eb84c0415744fda8429b11a64b730a2e1ef02335c05685a98946fffdfd5042aa3b6f2a23a645a454613acd364af612b09d
data/README.md CHANGED
@@ -74,9 +74,11 @@ The generator creates and runs a migration to add the necessary table to your
74
74
  database. It also mounts Maintenance Tasks in your `config/routes.rb`. By
75
75
  default the web UI can be accessed in the new `/maintenance_tasks` path.
76
76
 
77
- In case you use an exception reporting service (e.g. Bugsnag) you might want to
78
- define an error handler. See [Customizing the error
79
- handler](#customizing-the-error-handler) for more information.
77
+ This gem uses the [Rails Error Reporter][rails-error-reporting] to report errors.
78
+ If you are using a bug tracking service you may want to subscribe to the
79
+ reporter. See [Reporting Errors](#reporting-errors) for more information.
80
+
81
+ [rails-error-reporting]: https://guides.rubyonrails.org/error_reporting.html
80
82
 
81
83
  ### Active Job Dependency
82
84
 
@@ -114,12 +116,12 @@ The typical Maintenance Tasks workflow is as follows:
114
116
  1. [Generate a class describing the Task](#creating-a-task) and the work to be
115
117
  done.
116
118
  2. Run the Task
117
- - either by [using the included web UI](#running-a-task-from-the-web-ui),
118
- - or by [using the command line](#running-a-task-from-the-command-line),
119
- - or by [using Ruby](#running-a-task-from-ruby).
119
+ - either by [using the included web UI](#running-a-task-from-the-web-ui),
120
+ - or by [using the command line](#running-a-task-from-the-command-line),
121
+ - or by [using Ruby](#running-a-task-from-ruby).
120
122
  3. [Monitor the Task](#monitoring-your-tasks-status)
121
- - either by using the included web UI,
122
- - or by manually checking your task’s run’s status in your database.
123
+ - either by using the included web UI,
124
+ - or by manually checking your task’s run’s status in your database.
123
125
  4. Optionally, delete the Task code if you no longer need it.
124
126
 
125
127
  ### Creating a Task
@@ -168,7 +170,8 @@ end
168
170
  When processing records from an Active Record Relation, records are fetched in
169
171
  batches internally, and then each record is passed to the `#process` method.
170
172
  Maintenance Tasks will query the database to fetch records in batches of 100 by
171
- default, but the batch size can be modified using the `collection_batch_size` macro:
173
+ default, but the batch size can be modified using the `collection_batch_size`
174
+ macro:
172
175
 
173
176
  ```ruby
174
177
  # app/tasks/maintenance/update_posts_task.rb
@@ -502,13 +505,44 @@ set of values will be used to populate a dropdown in the user interface. The
502
505
  following types are supported:
503
506
 
504
507
  * Arrays
505
- * Procs and lambdas that optionally accept the Task instance, and return an Array.
506
- * Callable objects that receive one argument, the Task instance, and return an Array.
508
+ * Procs and lambdas that optionally accept the Task instance, and return an
509
+ Array.
510
+ * Callable objects that receive one argument, the Task instance, and return an
511
+ Array.
507
512
  * Methods that return an Array, called on the Task instance.
508
513
 
509
514
  For enumerables that don't match the supported types, a text field will be
510
515
  rendered instead.
511
516
 
517
+ ### Masking Task Parameters
518
+
519
+ Task attributes can be masked in the UI by adding `mask_attribute` class method
520
+ in the task class. This will replace the value in the arguments list with
521
+ `[FILTERED]` in the UI.
522
+
523
+ ```ruby
524
+ # app/tasks/maintenance/sensitive_params_task.rb
525
+
526
+ module Maintenance
527
+ class SensitiveParamsTask < MaintenanceTasks::Task
528
+ attribute :sensitive_content, :string
529
+
530
+ mask_attribute :sensitive_content
531
+ end
532
+ end
533
+ ```
534
+
535
+ If you have any filtered parameters in the global [Rails parameter
536
+ filter][rails-parameter-filter], they will be automatically taken into account
537
+ when masking the parameters, which means that you can mask parameters across all
538
+ tasks by adding them to the global rails parameters filter.
539
+
540
+ [rails-parameter-filter]:https://guides.rubyonrails.org/configuring.html#config-filter-parameters
541
+
542
+ ```ruby
543
+ Rails.application.config.filter_parameters += %i[token]
544
+ ```
545
+
512
546
  ### Custom cursor columns to improve performance
513
547
 
514
548
  The [job-iteration gem][job-iteration], on which this gem depends, adds an
@@ -899,10 +933,10 @@ a Task can be in:
899
933
 
900
934
  The Maintenance Tasks engine uses Rails sessions for flash messages and storing
901
935
  the CSRF token. For the engine to work in an API-only Rails application, you
902
- need to add a [session middleware][] and the `ActionDispatch::Flash`
903
- middleware. The engine also defines a strict [Content Security Policy][], make
904
- sure to include `ActionDispatch::ContentSecurityPolicy::Middleware` in your
905
- app's middleware stack to ensure the CSP is delivered to the user's browser.
936
+ need to add a [session middleware][] and the `ActionDispatch::Flash` middleware.
937
+ The engine also defines a strict [Content Security Policy][], make sure to
938
+ include `ActionDispatch::ContentSecurityPolicy::Middleware` in your app's
939
+ middleware stack to ensure the CSP is delivered to the user's browser.
906
940
 
907
941
  [session middleware]: https://guides.rubyonrails.org/api_app.html#using-session-middlewares
908
942
  [Content Security Policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
@@ -926,8 +960,8 @@ module YourApplication
926
960
  end
927
961
  ```
928
962
 
929
- You can read more in the [Using Rails for API-only Applications][rails api] Rails
930
- guide.
963
+ You can read more in the [Using Rails for API-only Applications][rails api]
964
+ Rails guide.
931
965
 
932
966
  [rails api]: https://guides.rubyonrails.org/api_app.html
933
967
 
@@ -944,9 +978,9 @@ infrastructure or code changes.
944
978
  This means a Task can safely be interrupted, re-enqueued and resumed without any
945
979
  intervention at the end of an iteration, after the `process` method returns.
946
980
 
947
- By default, a running Task will be interrupted after running for more 5 minutes.
948
- This is [configured in the `job-iteration` gem][max-job-runtime] and can be
949
- tweaked in an initializer if necessary.
981
+ By default, a running Task will be interrupted after running for more than 5
982
+ minutes. This is [configured in the `job-iteration` gem][max-job-runtime] and
983
+ can be tweaked in an initializer if necessary.
950
984
 
951
985
  [max-job-runtime]: https://github.com/Shopify/job-iteration/blob/-/guides/best-practices.md#max-job-runtime
952
986
 
@@ -988,44 +1022,58 @@ If you are stuck in `pausing` and wish to preserve your tasks's position
988
1022
  There are a few configurable options for the gem. Custom configurations should
989
1023
  be placed in a `maintenance_tasks.rb` initializer.
990
1024
 
991
- #### Customizing the error handler
1025
+ #### Reporting errors
992
1026
 
993
1027
  Exceptions raised while a Task is performing are rescued and information about
994
1028
  the error is persisted and visible in the UI.
995
1029
 
996
- If you want to integrate with an exception monitoring service (e.g. Bugsnag),
997
- you can define an error handler:
1030
+ Errors are also sent to the `Rails.error.reporter`, which can be configured by
1031
+ your application. See the [Error Reporting in Rails
1032
+ Applications][rails-error-reporting] guide for more details.
998
1033
 
999
- ```ruby
1000
- # config/initializers/maintenance_tasks.rb
1034
+ Reports to the error reporter will contain the following data:
1001
1035
 
1002
- MaintenanceTasks.error_handler = ->(error, task_context, _errored_element) do
1003
- Bugsnag.notify(error) do |notification|
1004
- notification.add_metadata(:task, task_context)
1005
- end
1036
+ * `error`: The exception that was raised.
1037
+ * `context`: A hash with additional information about the Task and the error:
1038
+ * `task_name`: The name of the Task that errored
1039
+ * `started_at`: The time the Task started
1040
+ * `ended_at`: The time the Task errored
1041
+ * `run_id`: The id of the errored Task run
1042
+ * `tick_count`: The tick count at the time of the error
1043
+ * `errored_element`: The element, if any, that was being processed when the
1044
+ * `source`: This will be `maintenance-tasks`
1045
+
1046
+ Note that `context` may be empty if the Task produced an error before any
1047
+ context could be gathered (for example, if deserializing the job to process your
1048
+ Task failed).
1049
+
1050
+ #### Reporting errors during iteration
1051
+
1052
+ By default, errors raised during task iteration will be raised to the
1053
+ application and iteration will stop. However, you may want to handle some errors
1054
+ and continue iteration. `MaintenanceTasks::Task.report_on` can be used to rescue
1055
+ certain exceptions and report them to the Rails error reporter. Any keyword
1056
+ arguments are passed to
1057
+ [ActiveSupport::ErrorReporter#report][as-error-reporter-report]:
1058
+
1059
+ [as-error-reporter-report]: https://api.rubyonrails.org/classes/ActiveSupport/ErrorReporter.html#method-i-report
1060
+
1061
+ ```ruby
1062
+ class MyTask < MaintenanceTasks::Task
1063
+ report_on(MyException, OtherException, severity: :info, context: {task_name: "my_task"})
1006
1064
  end
1007
1065
  ```
1008
1066
 
1009
- The error handler should be a lambda that accepts three arguments:
1067
+ `MaintenanceTasks::Task` also includes `ActiveSupport::Rescuable` which you can
1068
+ use to implement custom error handling.
1010
1069
 
1011
- * `error`: The exception that was raised.
1012
- * `task_context`: A hash with additional information about the Task and the
1013
- error:
1014
- * `task_name`: The name of the Task that errored
1015
- * `started_at`: The time the Task started
1016
- * `ended_at`: The time the Task errored
1017
-
1018
- Note that `task_context` may be empty if the Task produced an error before any
1019
- context could be gathered (for example, if deserializing the job to process
1020
- your Task failed).
1021
- * `errored_element`: The element, if any, that was being processed when the Task
1022
- raised an exception. If you would like to pass this object to your exception
1023
- monitoring service, make sure you **sanitize the object** to avoid leaking
1024
- sensitive data and **convert it to a format** that is compatible with your bug
1025
- tracker. For example, Bugsnag only sends the id and class name of Active
1026
- Record objects in order to protect sensitive data. CSV rows, on the other
1027
- hand, are converted to strings and passed raw to Bugsnag, so make sure to
1028
- filter any personal data from these objects before adding them to a report.
1070
+ ```ruby
1071
+ class MyTask < MaintenanceTasks::Task
1072
+ rescue_from(MyException) do |exception|
1073
+ handle(exception)
1074
+ end
1075
+ end
1076
+ ```
1029
1077
 
1030
1078
  #### Customizing the maintenance tasks module
1031
1079
 
@@ -8,16 +8,20 @@ module MaintenanceTasks
8
8
  BULMA_CDN = "https://cdn.jsdelivr.net"
9
9
 
10
10
  content_security_policy do |policy|
11
- policy.style_src(
11
+ policy.style_src_elem(
12
12
  BULMA_CDN,
13
- # ruby syntax highlighting
14
- "'sha256-y9V0na/WU44EUNI/HDP7kZ7mfEci4PAOIjYOOan6JMA='",
13
+ # <style> tag in app/views/layouts/maintenance_tasks/application.html.erb
14
+ "'sha256-WHHDQLdkleXnAN5zs0GDXC5ls41CHUaVsJtVpaNx+EM='",
15
15
  )
16
- policy.script_src(
17
- # page refresh script
16
+ policy.script_src_elem(
17
+ # <script> tag in app/views/layouts/maintenance_tasks/application.html.erb
18
18
  "'sha256-NiHKryHWudRC2IteTqmY9v1VkaDUA/5jhgXkMTkgo2w='",
19
19
  )
20
+
21
+ policy.require_trusted_types_for # disable because we use new DOMParser().parseFromString
20
22
  policy.frame_ancestors(:self)
23
+ policy.connect_src(:self)
24
+ policy.form_action(:self)
21
25
  end
22
26
 
23
27
  protect_from_forgery with: :exception
@@ -14,7 +14,7 @@ module MaintenanceTasks
14
14
  # @param datetime [ActiveSupport::TimeWithZone] the time to be presented.
15
15
  # @return [String] the HTML to render with the relative datetime in words.
16
16
  def time_ago(datetime)
17
- time_tag(datetime, title: datetime.utc.iso8601, class: "is-clickable") do
17
+ time_tag(datetime, title: datetime.utc, class: "is-clickable") do
18
18
  time_ago_in_words(datetime) + " ago"
19
19
  end
20
20
  end
@@ -47,7 +47,7 @@ module MaintenanceTasks
47
47
  progress_bar = tag.progress(
48
48
  value: progress.value,
49
49
  max: progress.max,
50
- class: ["progress"] + STATUS_COLOURS.fetch(run.status),
50
+ class: ["progress", "mt-4"] + STATUS_COLOURS.fetch(run.status),
51
51
  )
52
52
  progress_text = tag.p(tag.i(progress.text))
53
53
  tag.div(progress_bar + progress_text, class: "block")
@@ -60,7 +60,10 @@ module MaintenanceTasks
60
60
  # @return [String] the span element containing the status, with the
61
61
  # appropriate tag class attached.
62
62
  def status_tag(status)
63
- tag.span(status.capitalize, class: ["tag"] + STATUS_COLOURS.fetch(status))
63
+ tag.span(
64
+ status.capitalize,
65
+ class: ["tag", "has-text-weight-medium", "pr-2", "mr-4"] + STATUS_COLOURS.fetch(status),
66
+ )
64
67
  end
65
68
 
66
69
  # Reports the approximate elapsed time a Run has been processed so far based
@@ -101,7 +104,7 @@ module MaintenanceTasks
101
104
  )
102
105
  end
103
106
 
104
- # Resolves values covered by the inclusion validator for a task attribute.
107
+ # Resolves values covered by the inclusion validator for a Task attribute.
105
108
  # Supported option types:
106
109
  # - Arrays
107
110
  # - Procs and lambdas that optionally accept the Task instance, and return an Array.
@@ -112,7 +115,7 @@ module MaintenanceTasks
112
115
  #
113
116
  # Returned values are used to populate a dropdown list of options.
114
117
  #
115
- # @param task_class [Class<Task>] The task class for which the value needs to be resolved.
118
+ # @param task [Task] The Task for which the value needs to be resolved.
116
119
  # @param parameter_name [String] The parameter name.
117
120
  #
118
121
  # @return [Array] value of the resolved inclusion option.
@@ -149,24 +152,27 @@ module MaintenanceTasks
149
152
  # If the parameter has a `validates_inclusion_of` validator, return a dropdown list of options instead.
150
153
  def parameter_field(form_builder, parameter_name)
151
154
  inclusion_values = resolve_inclusion_value(form_builder.object, parameter_name)
152
- return form_builder.select(parameter_name, inclusion_values, prompt: "Select a value") if inclusion_values
155
+ if inclusion_values
156
+ return tag.div(form_builder.select(parameter_name, inclusion_values, prompt: "Select a value"), class: "select")
157
+ end
153
158
 
154
159
  case form_builder.object.class.attribute_types[parameter_name]
155
160
  when ActiveModel::Type::Integer
156
- form_builder.number_field(parameter_name)
161
+ form_builder.number_field(parameter_name, class: "input")
157
162
  when ActiveModel::Type::Decimal, ActiveModel::Type::Float
158
- form_builder.number_field(parameter_name, { step: "any" })
163
+ form_builder.number_field(parameter_name, { step: "any", class: "input" })
159
164
  when ActiveModel::Type::DateTime
160
- form_builder.datetime_field(parameter_name) + datetime_field_help_text
165
+ form_builder.datetime_field(parameter_name, class: "input") + datetime_field_help_text
161
166
  when ActiveModel::Type::Date
162
- form_builder.date_field(parameter_name)
167
+ form_builder.date_field(parameter_name, class: "input")
163
168
  when ActiveModel::Type::Time
164
- form_builder.time_field(parameter_name)
169
+ form_builder.time_field(parameter_name, class: "input")
165
170
  when ActiveModel::Type::Boolean
166
- form_builder.check_box(parameter_name)
171
+ form_builder.check_box(parameter_name, class: "checkbox")
167
172
  else
168
173
  form_builder.text_area(parameter_name, class: "textarea")
169
174
  end
175
+ .then { |input| tag.div(input, class: "control") }
170
176
  end
171
177
 
172
178
  # Return helper text for the datetime-local form field.
@@ -182,5 +188,16 @@ module MaintenanceTasks
182
188
  class: "content is-small",
183
189
  )
184
190
  end
191
+
192
+ # Checks if an attribute is required for a given Task.
193
+ #
194
+ # @param task [MaintenanceTasks::TaskDataShow] The TaskDataShow instance.
195
+ # @param parameter_name [Symbol] The name of the attribute to check.
196
+ # @return [Boolean] Whether the attribute is required.
197
+ def attribute_required?(task, parameter_name)
198
+ task.class.validators_on(parameter_name).any? do |validator|
199
+ validator.kind == :presence
200
+ end
201
+ end
185
202
  end
186
203
  end
@@ -112,7 +112,7 @@ module MaintenanceTasks
112
112
  end
113
113
  rescue => error
114
114
  @errored_element = input
115
- raise error
115
+ raise error unless @task.rescue_with_handler(error)
116
116
  end
117
117
 
118
118
  def before_perform
@@ -181,11 +181,20 @@ module MaintenanceTasks
181
181
  task_name: @run.task_name,
182
182
  started_at: @run.started_at,
183
183
  ended_at: @run.ended_at,
184
+ run_id: @run.id,
185
+ tick_count: @run.tick_count,
184
186
  }
185
187
  end
186
- errored_element = @errored_element if defined?(@errored_element)
188
+ task_context[:errored_element] = @errored_element if defined?(@errored_element)
187
189
  ensure
188
- MaintenanceTasks.error_handler.call(error, task_context, errored_element)
190
+ if MaintenanceTasks.instance_variable_get(:@error_handler)
191
+ errored_element = task_context.delete(:errored_element)
192
+ MaintenanceTasks.error_handler.call(error, task_context.except(:run_id, :tick_count), errored_element)
193
+ elsif Rails.gem_version >= Gem::Version.new("7.1")
194
+ Rails.error.report(error, context: task_context, source: "maintenance-tasks")
195
+ else
196
+ Rails.error.report(error, handled: true, context: task_context)
197
+ end
189
198
  end
190
199
  end
191
200
  end
@@ -33,11 +33,7 @@ module MaintenanceTasks
33
33
  ]
34
34
  COMPLETED_STATUSES = [:succeeded, :errored, :cancelled]
35
35
 
36
- if Rails.gem_version >= Gem::Version.new("7.0.alpha")
37
- enum :status, STATUSES.to_h { |status| [status, status.to_s] }
38
- else
39
- enum status: STATUSES.to_h { |status| [status, status.to_s] }
40
- end
36
+ enum :status, STATUSES.to_h { |status| [status, status.to_s] }
41
37
 
42
38
  after_save :instrument_status_change
43
39
 
@@ -426,12 +422,23 @@ module MaintenanceTasks
426
422
  if task.attribute_names.any? && arguments.present?
427
423
  task.assign_attributes(arguments)
428
424
  end
425
+
426
+ task.metadata = metadata
429
427
  task
430
428
  rescue ActiveModel::UnknownAttributeError
431
429
  task
432
430
  end
433
431
  end
434
432
 
433
+ # Returns all the run arguments with sensitive information masked.
434
+ #
435
+ # @return [Hash] The masked arguments.
436
+ def masked_arguments
437
+ return unless arguments.present?
438
+
439
+ argument_filter.filter(arguments)
440
+ end
441
+
435
442
  private
436
443
 
437
444
  def instrument_status_change
@@ -486,5 +493,11 @@ module MaintenanceTasks
486
493
 
487
494
  value&.first(limit)
488
495
  end
496
+
497
+ def argument_filter
498
+ @argument_filter ||= ActiveSupport::ParameterFilter.new(
499
+ Rails.application.config.filter_parameters + task.masked_arguments,
500
+ )
501
+ end
489
502
  end
490
503
  end
@@ -8,6 +8,7 @@ module MaintenanceTasks
8
8
  include ActiveModel::Attributes
9
9
  include ActiveModel::AttributeAssignment
10
10
  include ActiveModel::Validations
11
+ include ActiveSupport::Rescuable
11
12
 
12
13
  class NotFoundError < NameError; end
13
14
 
@@ -27,8 +28,15 @@ module MaintenanceTasks
27
28
  # @api private
28
29
  class_attribute :collection_builder_strategy, default: NullCollectionBuilder.new
29
30
 
31
+ # The sensitive attributes that will be filtered when fetching a run.
32
+ #
33
+ # @api private
34
+ class_attribute :masked_arguments, default: []
35
+
30
36
  define_callbacks :start, :complete, :error, :cancel, :pause, :interrupt
31
37
 
38
+ attr_accessor :metadata
39
+
32
40
  class << self
33
41
  # Finds a Task with the given name.
34
42
  #
@@ -152,6 +160,13 @@ module MaintenanceTasks
152
160
  self.active_record_enumerator_batch_size = size
153
161
  end
154
162
 
163
+ # Adds attribute names to sensitive arguments list.
164
+ #
165
+ # @param attributes [Array<Symbol>] the attribute names to filter.
166
+ def mask_attribute(*attributes)
167
+ self.masked_arguments += attributes
168
+ end
169
+
155
170
  # Initialize a callback to run after the task starts.
156
171
  #
157
172
  # @param filter_list apply filters to the callback
@@ -200,6 +215,22 @@ module MaintenanceTasks
200
215
  set_callback(:error, :after, *filter_list, &block)
201
216
  end
202
217
 
218
+ # Rescue listed exceptions during an iteration and report them to the error reporter, then
219
+ # continue iteration.
220
+ #
221
+ # @param exceptions list of exceptions to rescue and report
222
+ # @param report_options [Hash] optionally, supply additional options for `Rails.error.report`.
223
+ # By default: <code>{ source: "maintenance_tasks" }</code> or (Rails <v7.1) <code>{ handled: true }</code>.
224
+ def report_on(*exceptions, **report_options)
225
+ rescue_from(*exceptions) do |exception|
226
+ if Rails.gem_version >= Gem::Version.new("7.1")
227
+ Rails.error.report(exception, source: "maintenance_tasks", **report_options)
228
+ else
229
+ Rails.error.report(exception, handled: true, **report_options)
230
+ end
231
+ end
232
+ end
233
+
203
234
  private
204
235
 
205
236
  def load_constants
@@ -34,7 +34,8 @@ module MaintenanceTasks
34
34
  end
35
35
 
36
36
  completed_runs = Run.completed.where(task_name: task_names)
37
- last_runs = Run.with_attached_csv.where(id: completed_runs.select("MAX(id) as id").group(:task_name))
37
+ last_runs = Run.with_attached_csv
38
+ .where(created_at: completed_runs.select("MAX(created_at) as created_at").group(:task_name))
38
39
  task_names.map do |task_name|
39
40
  last_run = last_runs.find { |run| run.task_name == task_name }
40
41
  tasks << TaskDataIndex.new(task_name, last_run)
@@ -1,4 +1,4 @@
1
- <nav class="navbar is-dark is-spaced" role="navigation" aria-label="main navigation">
1
+ <nav class="navbar is-light" role="navigation" aria-label="main navigation">
2
2
  <div class="navbar-brand">
3
3
  <%= link_to 'Maintenance Tasks', root_path, class: 'navbar-item is-size-4 has-text-weight-semibold has-text-danger' %>
4
4
  </div>
@@ -15,10 +15,10 @@
15
15
  <%= csrf_meta_tags %>
16
16
 
17
17
  <%=
18
- stylesheet_link_tag(URI.join(controller.class::BULMA_CDN, 'npm/bulma@0.9.4/css/bulma.css'),
18
+ stylesheet_link_tag(URI.join(controller.class::BULMA_CDN, "npm/bulma@1.0.3/css/versions/bulma-no-dark-mode.min.css"),
19
19
  media: :all,
20
- integrity: 'sha384-qQlNh1kc0FyhUqUDXKkl5wpiiSm8PXQw2ZWhAVfU46tmdMDfq2vXG2CXWYT+Dls3',
21
- crossorigin: 'anonymous') unless request.xhr?
20
+ integrity: "sha256-HCNMQcqH/4MnGR0EYg2S3/BXYMM1z9lrFV10ANRd79o",
21
+ crossorigin: "anonymous") unless request.xhr?
22
22
  %>
23
23
 
24
24
  <style>
@@ -29,6 +29,36 @@
29
29
  .ruby-ivar, .ruby-cvar, .ruby-gvar, .ruby-int, .ruby-imaginary, .ruby-float, .ruby-rational { color: #005cc5; }
30
30
  .ruby-kw { color: #d73a49; }
31
31
  .ruby-label, .ruby-tstring-beg, .ruby-tstring-content, .ruby-tstring-end { color: #032f62; }
32
+
33
+ .select, select { width: 100%; }
34
+ summary { cursor: pointer; }
35
+ input[type="datetime-local"], input[type="date"], input[type="time"] {
36
+ width: fit-content;
37
+ }
38
+ details > summary {
39
+ list-style: none;
40
+ }
41
+ summary::-webkit-details-marker {
42
+ display: none
43
+ }
44
+ summary::before {
45
+ content: '► ';
46
+ position:absolute;
47
+ font-size: 16px
48
+ }
49
+ details[open] summary:before {
50
+ content: "▼ ";
51
+ }
52
+
53
+ .box {
54
+ box-shadow: 0 4px 6px -1px #0000001a,
55
+ 0 2px 4px -2px #0000001a;
56
+ }
57
+ .label.is-required:after {
58
+ content: " (required)";
59
+ color: #ff6685;
60
+ font-size: 12px;
61
+ }
32
62
  </style>
33
63
 
34
64
  <script>
@@ -1,4 +1,4 @@
1
1
  <% if arguments.present? %>
2
- <h6 class="title is-6">Arguments:</h6>
2
+ <h6 class="title is-6 has-text-weight-medium">Arguments:</h6>
3
3
  <%= render "maintenance_tasks/runs/serializable", serializable: arguments %>
4
4
  <% end %>
@@ -1,9 +1,15 @@
1
- <div class="box">
2
- <h5 class="title is-5">
3
- <%= time_tag run.created_at, title: run.created_at.utc.iso8601 %>
4
- <%= status_tag run.status %>
5
- <span class="is-pulled-right" title="Run ID">#<%= run.id %></span>
6
- </h5>
1
+ <details class="box" open id="run_<%= run.id %>">
2
+ <summary class="is-flex is-justify-content-space-between is-align-items-center">
3
+ <div class="is-flex is-align-items-center">
4
+ <h5 class="title is-5 has-text-weight-medium pl-5 pr-5 mb-0">
5
+ <%= time_tag run.created_at, title: run.created_at.utc %>
6
+ </h5>
7
+ <%= status_tag run.status %>
8
+ </div>
9
+ <div>
10
+ <a href="#run_<%= run.id %>" class="is-pulled-right" title="Run ID">#<%= run.id %></a>
11
+ </div>
12
+ </summary>
7
13
 
8
14
  <%= progress run %>
9
15
 
@@ -17,7 +23,7 @@
17
23
 
18
24
  <%= render "maintenance_tasks/runs/csv", run: run %>
19
25
  <%= tag.hr if run.csv_file.present? && run.arguments.present? %>
20
- <%= render "maintenance_tasks/runs/arguments", arguments: run.arguments %>
26
+ <%= render "maintenance_tasks/runs/arguments", arguments: run.masked_arguments %>
21
27
  <%= tag.hr if run.csv_file.present? || run.arguments.present? && run.metadata.present? %>
22
28
  <%= render "maintenance_tasks/runs/metadata", metadata: run.metadata %>
23
29
 
@@ -42,4 +48,4 @@
42
48
  <%= button_to 'Cancel', cancel_task_run_path(@task, run), class: 'button is-danger' %>
43
49
  <% end%>
44
50
  </div>
45
- </div>
51
+ </details>
@@ -1,24 +1,21 @@
1
1
  <% if serializable.present? %>
2
2
  <% case serializable %>
3
3
  <% when Hash %>
4
- <div class="table-container">
5
- <table class="table">
6
- <tbody>
7
- <% serializable.transform_values(&:to_s).each do |key, value| %>
8
- <tr>
9
- <td class="is-family-monospace"><%= key %></td>
10
- <td>
11
- <% next if value.empty? %>
12
- <% if value.include?("\n") %>
13
- <pre><%= value %></pre>
14
- <% else %>
15
- <code><%= value %></code>
16
- <% end %>
17
- </td>
18
- </tr>
19
- <% end %>
20
- </tbody>
21
- </table>
4
+ <div class="arguments-container grid is-col-min-15">
5
+ <% serializable.transform_values(&:to_s).each do |key, value| %>
6
+ <div class="cell mb-4">
7
+ <div class="is-family-monospace mb-2"><%= key %></div>
8
+ <div class="is-flex justify-content">
9
+ <% if !value.empty? %>
10
+ <% if value.include?("\n") %>
11
+ <pre><%= value %></pre>
12
+ <% else %>
13
+ <code><%= value %></code>
14
+ <% end %>
15
+ <% end %>
16
+ </div>
17
+ </div>
18
+ <% end %>
22
19
  </div>
23
20
  <% else %>
24
21
  <code><%= serializable.inspect %></code>
@@ -1,12 +1,12 @@
1
- <div class="box">
2
- <h3 class="title is-3">
1
+ <div class="cell box">
2
+ <h3 class="title is-5 has-text-weight-medium">
3
3
  <%= link_to task, task_path(task) %>
4
4
  <%= status_tag(task.status) %>
5
5
  </h3>
6
6
 
7
7
  <% if (run = task.related_run) %>
8
- <h5 class="title is-5">
9
- <%= time_tag run.created_at, title: run.created_at %>
8
+ <h5 class="title is-5 has-text-weight-medium">
9
+ <%= time_tag run.created_at, title: run.created_at.utc %>
10
10
  </h5>
11
11
 
12
12
  <%= progress run %>
@@ -21,7 +21,7 @@
21
21
 
22
22
  <%= render "maintenance_tasks/runs/csv", run: run %>
23
23
  <%= tag.hr if run.csv_file.present? && run.arguments.present? %>
24
- <%= render "maintenance_tasks/runs/arguments", arguments: run.arguments %>
24
+ <%= render "maintenance_tasks/runs/arguments", arguments: run.masked_arguments %>
25
25
  <%= tag.hr if run.csv_file.present? || run.arguments.present? && run.metadata.present? %>
26
26
  <%= render "maintenance_tasks/runs/metadata", metadata: run.metadata %>
27
27
  <% end %>
@@ -9,15 +9,17 @@
9
9
  </div>
10
10
  <% else %>
11
11
  <% if active_tasks = @available_tasks[:active] %>
12
- <h3 class="title is-3">Active Tasks</h3>
12
+ <h3 class="title is-4 has-text-weight-bold">Active Tasks</h3>
13
13
  <%= render partial: 'task', collection: active_tasks %>
14
14
  <% end %>
15
15
  <% if new_tasks = @available_tasks[:new] %>
16
- <h3 class="title is-3">New Tasks</h3>
17
- <%= render partial: 'task', collection: new_tasks %>
16
+ <h3 class="title is-4 has-text-weight-bold">New Tasks</h3>
17
+ <div class="grid is-col-min-20">
18
+ <%= render partial: 'task', collection: new_tasks %>
19
+ </div>
18
20
  <% end %>
19
21
  <% if completed_tasks = @available_tasks[:completed] %>
20
- <h3 class="title is-3">Completed Tasks</h3>
22
+ <h3 class="title is-4 has-text-weight-bold">Completed Tasks</h3>
21
23
  <%= render partial: 'task', collection: completed_tasks %>
22
24
  <% end %>
23
25
  <% end %>
@@ -1,27 +1,25 @@
1
1
  <% content_for :page_title, @task %>
2
2
 
3
- <h1 class="title is-1">
3
+ <h1 class="title is-3 has-text-weight-bold">
4
4
  <%= @task %>
5
5
  </h1>
6
6
 
7
- <div class="buttons">
7
+ <div class="container">
8
8
  <%= form_with url: task_runs_path(@task), method: :post do |form| %>
9
9
  <% if @task.csv_task? %>
10
- <div class="block">
11
- <%= form.label :csv_file %>
10
+ <div class="container mb-4">
11
+ <%= form.label :csv_file, class: "label" %>
12
12
  <%= form.file_field :csv_file, accept: "text/csv" %>
13
13
  </div>
14
14
  <% end %>
15
15
  <% parameter_names = @task.parameter_names %>
16
16
  <% if parameter_names.any? %>
17
- <div class="block">
17
+ <div class="grid is-col-min-15">
18
18
  <%= fields_for :task, @task.new do |ff| %>
19
19
  <% parameter_names.each do |parameter_name| %>
20
- <div class="field">
21
- <%= ff.label parameter_name, parameter_name, class: "label is-family-monospace" %>
22
- <div class="control">
23
- <%= parameter_field(ff, parameter_name) %>
24
- </div>
20
+ <div class="cell">
21
+ <%= ff.label parameter_name, parameter_name, class: ["label", "is-family-monospace", { "is-required": attribute_required?(ff.object, parameter_name) }] %>
22
+ <%= parameter_field(ff, parameter_name) %>
25
23
  </div>
26
24
  <% end %>
27
25
  <% end %>
@@ -29,13 +27,19 @@
29
27
  <% end %>
30
28
  <%= render "maintenance_tasks/tasks/custom", form: form %>
31
29
  <div class="block">
32
- <%= form.submit 'Run', class: "button is-success", disabled: @task.deleted? %>
30
+ <%= form.submit 'Run', class: "button is-success is-rounded mb-4 has-text-white-ter", disabled: @task.deleted? %>
33
31
  </div>
34
32
  <% end %>
35
33
  </div>
36
34
 
37
35
  <% if (code = @task.code) %>
36
+
37
+ <details class="box">
38
+ <summary class="is-size-5 is-flex is-align-items-center">
39
+ <h5 class="pl-5">Source code</h5>
40
+ </summary>
38
41
  <pre><code><%= highlight_code(code) %></code></pre>
42
+ </details>
39
43
  <% end %>
40
44
 
41
45
  <%= tag.div(data: { refresh: @task.refresh? || "" }) do %>
@@ -50,7 +54,7 @@
50
54
  <% if @task.runs_page.records.present? %>
51
55
  <hr/>
52
56
 
53
- <h4 class="title is-4">Previous Runs</h4>
57
+ <h4 class="title is-5 has-text-weight-bold">Previous Runs</h4>
54
58
 
55
59
  <%= render partial: "maintenance_tasks/runs/run", collection: @task.runs_page.records %>
56
60
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  class CreateMaintenanceTasksRuns < ActiveRecord::Migration[6.0]
4
4
  def change
5
- create_table(:maintenance_tasks_runs) do |t|
5
+ create_table(:maintenance_tasks_runs, id: primary_key_type) do |t|
6
6
  t.string(:task_name, null: false)
7
7
  t.datetime(:started_at)
8
8
  t.datetime(:ended_at)
@@ -20,4 +20,12 @@ class CreateMaintenanceTasksRuns < ActiveRecord::Migration[6.0]
20
20
  t.index([:task_name, :created_at], order: { created_at: :desc })
21
21
  end
22
22
  end
23
+
24
+ private
25
+
26
+ def primary_key_type
27
+ config = Rails.configuration.generators
28
+ setting = config.options[config.orm][:primary_key_type]
29
+ setting || :primary_key
30
+ end
23
31
  end
@@ -21,6 +21,12 @@ module MaintenanceTasks
21
21
  MaintenanceTasks.backtrace_cleaner = Rails.backtrace_cleaner
22
22
  end
23
23
 
24
+ if Rails.gem_version >= Gem::Version.new("7.1")
25
+ initializer "maintenance_tasks.deprecator" do
26
+ Rails.application.deprecators[:maintenance_tasks] = MaintenanceTasks.deprecator
27
+ end
28
+ end
29
+
24
30
  config.to_prepare do
25
31
  _ = TaskJobConcern # load this for JobIteration compatibility check
26
32
  end
@@ -8,8 +8,6 @@ require "active_record"
8
8
  require "job-iteration"
9
9
  require "maintenance_tasks/engine"
10
10
 
11
- require "patches/active_record_batch_enumerator"
12
-
13
11
  # The engine's namespace module. It provides isolation between the host
14
12
  # application's code and the engine-specific code. Top-level engine constants
15
13
  # and variables are defined under this module.
@@ -63,16 +61,6 @@ module MaintenanceTasks
63
61
  # use when cleaning a Run's backtrace.
64
62
  mattr_accessor :backtrace_cleaner
65
63
 
66
- # @!attribute error_handler
67
- # @scope class
68
- #
69
- # The callback to perform when an error occurs in the Task. See the
70
- # {file:README#label-Customizing+the+error+handler} for details.
71
- #
72
- # @return [Proc] the callback to perform when an error occurs in the Task.
73
- mattr_accessor :error_handler, default:
74
- ->(_error, _task_context, _errored_element) {}
75
-
76
64
  # @!attribute parent_controller
77
65
  # @scope class
78
66
  #
@@ -96,4 +84,30 @@ module MaintenanceTasks
96
84
  #
97
85
  # @return [ActiveSupport::Duration] the threshold in seconds after which a task is considered stuck.
98
86
  mattr_accessor :stuck_task_duration, default: 5.minutes
87
+
88
+ class << self
89
+ DEPRECATION_MESSAGE = "MaintenanceTasks.error_handler is deprecated and will be removed in the 3.0 release. " \
90
+ "Instead, reports will be sent to the Rails error reporter. Do not set a handler and subscribe " \
91
+ "to the error reporter instead."
92
+ private_constant :DEPRECATION_MESSAGE
93
+
94
+ # @deprecated
95
+ def error_handler
96
+ deprecator.warn(DEPRECATION_MESSAGE)
97
+
98
+ @error_handler
99
+ end
100
+
101
+ # @deprecated
102
+ def error_handler=(proc)
103
+ deprecator.warn(DEPRECATION_MESSAGE)
104
+
105
+ @error_handler = proc
106
+ end
107
+
108
+ # @api-private
109
+ def deprecator
110
+ @deprecator ||= ActiveSupport::Deprecation.new("3.0", "MaintenanceTasks")
111
+ end
112
+ end
99
113
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maintenance_tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.10.1
4
+ version: 2.12.0
5
5
  platform: ruby
6
- original_platform: ''
7
6
  authors:
8
7
  - Shopify Engineering
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-12-18 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: actionpack
@@ -16,42 +15,42 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '6.1'
18
+ version: '7.0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '6.1'
25
+ version: '7.0'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: activejob
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: '6.1'
32
+ version: '7.0'
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
- version: '6.1'
39
+ version: '7.0'
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: activerecord
43
42
  requirement: !ruby/object:Gem::Requirement
44
43
  requirements:
45
44
  - - ">="
46
45
  - !ruby/object:Gem::Version
47
- version: '6.1'
46
+ version: '7.0'
48
47
  type: :runtime
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
52
51
  - - ">="
53
52
  - !ruby/object:Gem::Version
54
- version: '6.1'
53
+ version: '7.0'
55
54
  - !ruby/object:Gem::Dependency
56
55
  name: csv
57
56
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +85,14 @@ dependencies:
86
85
  requirements:
87
86
  - - ">="
88
87
  - !ruby/object:Gem::Version
89
- version: '6.1'
88
+ version: '7.0'
90
89
  type: :runtime
91
90
  prerelease: false
92
91
  version_requirements: !ruby/object:Gem::Requirement
93
92
  requirements:
94
93
  - - ">="
95
94
  - !ruby/object:Gem::Version
96
- version: '6.1'
95
+ version: '7.0'
97
96
  - !ruby/object:Gem::Dependency
98
97
  name: zeitwerk
99
98
  requirement: !ruby/object:Gem::Requirement
@@ -178,13 +177,12 @@ files:
178
177
  - lib/maintenance_tasks.rb
179
178
  - lib/maintenance_tasks/cli.rb
180
179
  - lib/maintenance_tasks/engine.rb
181
- - lib/patches/active_record_batch_enumerator.rb
182
180
  - lib/tasks/maintenance_tasks_tasks.rake
183
181
  homepage: https://github.com/Shopify/maintenance_tasks
184
182
  licenses:
185
183
  - MIT
186
184
  metadata:
187
- source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.10.1
185
+ source_code_uri: https://github.com/Shopify/maintenance_tasks/tree/v2.12.0
188
186
  allowed_push_host: https://rubygems.org
189
187
  rdoc_options: []
190
188
  require_paths:
@@ -200,7 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
200
198
  - !ruby/object:Gem::Version
201
199
  version: '0'
202
200
  requirements: []
203
- rubygems_version: 3.6.1
201
+ rubygems_version: 3.6.8
204
202
  specification_version: 4
205
203
  summary: A Rails engine for queuing and managing maintenance tasks
206
204
  test_files: []
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- if Rails.gem_version < Gem::Version.new("7.0")
4
- # Add attribute readers.
5
- module ActiveRecordBatchEnumerator
6
- # The primary key value from which the BatchEnumerator starts,
7
- # inclusive of the value.
8
- attr_reader :start
9
-
10
- # The primary key value at which the BatchEnumerator ends,
11
- # inclusive of the value.
12
- attr_reader :finish
13
-
14
- # The relation from which the BatchEnumerator yields batches.
15
- attr_reader :relation
16
-
17
- # The size of the batches yielded by the BatchEnumerator.
18
- def batch_size
19
- @of
20
- end
21
- end
22
-
23
- ActiveRecord::Batches::BatchEnumerator.include(ActiveRecordBatchEnumerator)
24
- end