maintenance_tasks 2.10.1 → 2.13.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 +160 -45
  3. data/app/controllers/maintenance_tasks/application_controller.rb +15 -5
  4. data/app/helpers/maintenance_tasks/application_helper.rb +1 -1
  5. data/app/helpers/maintenance_tasks/tasks_helper.rb +28 -11
  6. data/app/jobs/concerns/maintenance_tasks/task_job_concern.rb +33 -4
  7. data/app/models/maintenance_tasks/run.rb +103 -71
  8. data/app/models/maintenance_tasks/task.rb +42 -0
  9. data/app/models/maintenance_tasks/task_data_index.rb +2 -1
  10. data/app/views/layouts/maintenance_tasks/_navbar.html.erb +1 -1
  11. data/app/views/layouts/maintenance_tasks/application.html.erb +35 -4
  12. data/app/views/maintenance_tasks/runs/_arguments.html.erb +1 -1
  13. data/app/views/maintenance_tasks/runs/_run.html.erb +14 -8
  14. data/app/views/maintenance_tasks/runs/_serializable.html.erb +15 -18
  15. data/app/views/maintenance_tasks/tasks/_task.html.erb +5 -5
  16. data/app/views/maintenance_tasks/tasks/index.html.erb +6 -4
  17. data/app/views/maintenance_tasks/tasks/show.html.erb +16 -12
  18. data/db/migrate/20201211151756_create_maintenance_tasks_runs.rb +10 -2
  19. data/db/migrate/20210219212931_change_cursor_to_string.rb +1 -1
  20. data/db/migrate/20210225152418_remove_index_on_task_name.rb +1 -1
  21. data/db/migrate/20210517131953_add_arguments_to_maintenance_tasks_runs.rb +1 -1
  22. data/db/migrate/20211210152329_add_lock_version_to_maintenance_tasks_runs.rb +1 -1
  23. data/db/migrate/20220706101937_change_runs_tick_columns_to_bigints.rb +1 -1
  24. data/db/migrate/20220713131925_add_index_on_task_name_and_status_to_runs.rb +1 -1
  25. data/db/migrate/20230622035229_add_metadata_to_runs.rb +1 -1
  26. data/lib/maintenance_tasks/engine.rb +4 -0
  27. data/lib/maintenance_tasks.rb +50 -12
  28. metadata +13 -15
  29. data/lib/patches/active_record_batch_enumerator.rb +0 -24
@@ -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
 
@@ -48,15 +44,9 @@ module MaintenanceTasks
48
44
 
49
45
  attr_readonly :task_name
50
46
 
51
- if Rails.gem_version >= Gem::Version.new("7.1.alpha")
52
- serialize :backtrace, coder: YAML
53
- serialize :arguments, coder: JSON
54
- serialize :metadata, coder: JSON
55
- else
56
- serialize :backtrace
57
- serialize :arguments, JSON
58
- serialize :metadata, JSON
59
- end
47
+ serialize :backtrace, coder: YAML
48
+ serialize :arguments, coder: JSON
49
+ serialize :metadata, coder: JSON
60
50
 
61
51
  scope :active, -> { where(status: ACTIVE_STATUSES) }
62
52
  scope :completed, -> { where(status: COMPLETED_STATUSES) }
@@ -83,11 +73,10 @@ module MaintenanceTasks
83
73
  # Rescues and retries status transition if an ActiveRecord::StaleObjectError
84
74
  # is encountered.
85
75
  def enqueued!
86
- status_will_change!
87
- super
88
- rescue ActiveRecord::StaleObjectError
89
- reload_status
90
- retry
76
+ with_stale_object_retry do
77
+ status_will_change!
78
+ super
79
+ end
91
80
  end
92
81
 
93
82
  CALLBACKS_TRANSITION = {
@@ -96,23 +85,39 @@ module MaintenanceTasks
96
85
  paused: :pause,
97
86
  succeeded: :complete,
98
87
  }.transform_keys(&:to_s)
99
- private_constant :CALLBACKS_TRANSITION
88
+
89
+ DELAYS_PER_ATTEMPT = [0.1, 0.2, 0.4, 0.8, 1.6]
90
+ MAX_RETRIES = DELAYS_PER_ATTEMPT.size
91
+
92
+ private_constant :CALLBACKS_TRANSITION, :DELAYS_PER_ATTEMPT, :MAX_RETRIES
100
93
 
101
94
  # Saves the run, persisting the transition of its status, and all other
102
95
  # changes to the object.
103
96
  def persist_transition
104
- save!
97
+ retry_count = 0
98
+ begin
99
+ save!
100
+ rescue ActiveRecord::StaleObjectError
101
+ if retry_count < MAX_RETRIES
102
+ sleep(DELAYS_PER_ATTEMPT[retry_count])
103
+ retry_count += 1
104
+
105
+ success = succeeded?
106
+ reload_status
107
+ if success
108
+ self.status = :succeeded
109
+ else
110
+ job_shutdown
111
+ end
112
+
113
+ retry
114
+ else
115
+ raise
116
+ end
117
+ end
118
+
105
119
  callback = CALLBACKS_TRANSITION[status]
106
120
  run_task_callbacks(callback) if callback
107
- rescue ActiveRecord::StaleObjectError
108
- success = succeeded?
109
- reload_status
110
- if success
111
- self.status = :succeeded
112
- else
113
- job_shutdown
114
- end
115
- retry
116
121
  end
117
122
 
118
123
  # Increments +tick_count+ by +number_of_ticks+ and +time_running+ by
@@ -130,29 +135,23 @@ module MaintenanceTasks
130
135
  time_running: duration,
131
136
  touch: true,
132
137
  )
133
- if locking_enabled?
134
- locking_column = self.class.locking_column
135
- self[locking_column] += 1
136
- clear_attribute_change(locking_column)
137
- end
138
138
  end
139
139
 
140
140
  # Marks the run as errored and persists the error data.
141
141
  #
142
142
  # @param error [StandardError] the Error being persisted.
143
143
  def persist_error(error)
144
- self.started_at ||= Time.now
145
- update!(
146
- status: :errored,
147
- error_class: truncate(:error_class, error.class.name),
148
- error_message: truncate(:error_message, error.message),
149
- backtrace: MaintenanceTasks.backtrace_cleaner.clean(error.backtrace),
150
- ended_at: Time.now,
151
- )
144
+ with_stale_object_retry do
145
+ self.started_at ||= Time.now
146
+ update!(
147
+ status: :errored,
148
+ error_class: truncate(:error_class, error.class.name),
149
+ error_message: truncate(:error_message, error.message),
150
+ backtrace: MaintenanceTasks.backtrace_cleaner.clean(error.backtrace),
151
+ ended_at: Time.now,
152
+ )
153
+ end
152
154
  run_error_callback
153
- rescue ActiveRecord::StaleObjectError
154
- reload_status
155
- retry
156
155
  end
157
156
 
158
157
  # Refreshes the status and lock version attributes on the Active Record
@@ -245,11 +244,8 @@ module MaintenanceTasks
245
244
  # is encountered.
246
245
  def running
247
246
  if locking_enabled?
248
- begin
247
+ with_stale_object_retry do
249
248
  running! unless stopping?
250
- rescue ActiveRecord::StaleObjectError
251
- reload_status
252
- retry
253
249
  end
254
250
  else
255
251
  # Preserve swap-and-replace solution for data races until users
@@ -272,11 +268,11 @@ module MaintenanceTasks
272
268
  # @param count [Integer] the total iterations to be performed, as
273
269
  # specified by the Task.
274
270
  def start(count)
275
- update!(started_at: Time.now, tick_total: count)
271
+ with_stale_object_retry do
272
+ update!(started_at: Time.now, tick_total: count)
273
+ end
274
+
276
275
  task.run_callbacks(:start)
277
- rescue ActiveRecord::StaleObjectError
278
- reload_status
279
- retry
280
276
  end
281
277
 
282
278
  # Handles transitioning the status on a Run when the job shuts down.
@@ -311,16 +307,15 @@ module MaintenanceTasks
311
307
  # minutes ago, it will transition to cancelled, and the ended_at timestamp
312
308
  # will be updated.
313
309
  def cancel
314
- if paused? || stuck?
315
- self.status = :cancelled
316
- self.ended_at = Time.now
317
- persist_transition
318
- else
319
- cancelling!
310
+ with_stale_object_retry do
311
+ if paused? || stuck?
312
+ self.status = :cancelled
313
+ self.ended_at = Time.now
314
+ persist_transition
315
+ else
316
+ cancelling!
317
+ end
320
318
  end
321
- rescue ActiveRecord::StaleObjectError
322
- reload_status
323
- retry
324
319
  end
325
320
 
326
321
  # Marks a Run as pausing.
@@ -331,15 +326,14 @@ module MaintenanceTasks
331
326
  # Rescues and retries status transition if an ActiveRecord::StaleObjectError
332
327
  # is encountered.
333
328
  def pause
334
- if stuck?
335
- self.status = :paused
336
- persist_transition
337
- else
338
- pausing!
329
+ with_stale_object_retry do
330
+ if stuck?
331
+ self.status = :paused
332
+ persist_transition
333
+ else
334
+ pausing!
335
+ end
339
336
  end
340
- rescue ActiveRecord::StaleObjectError
341
- reload_status
342
- retry
343
337
  end
344
338
 
345
339
  # Returns whether a Run is stuck, which is defined as having a status of
@@ -426,12 +420,23 @@ module MaintenanceTasks
426
420
  if task.attribute_names.any? && arguments.present?
427
421
  task.assign_attributes(arguments)
428
422
  end
423
+
424
+ task.metadata = metadata
429
425
  task
430
426
  rescue ActiveModel::UnknownAttributeError
431
427
  task
432
428
  end
433
429
  end
434
430
 
431
+ # Returns all the run arguments with sensitive information masked.
432
+ #
433
+ # @return [Hash] The masked arguments.
434
+ def masked_arguments
435
+ return unless arguments.present?
436
+
437
+ argument_filter.filter(arguments)
438
+ end
439
+
435
440
  private
436
441
 
437
442
  def instrument_status_change
@@ -486,5 +491,32 @@ module MaintenanceTasks
486
491
 
487
492
  value&.first(limit)
488
493
  end
494
+
495
+ def argument_filter
496
+ @argument_filter ||= ActiveSupport::ParameterFilter.new(
497
+ Rails.application.config.filter_parameters + task.masked_arguments,
498
+ )
499
+ end
500
+
501
+ def with_stale_object_retry(retry_count = 0)
502
+ yield
503
+ rescue ActiveRecord::StaleObjectError
504
+ if retry_count < MAX_RETRIES
505
+ sleep(stale_object_retry_delay(retry_count))
506
+ retry_count += 1
507
+ reload_status
508
+
509
+ retry
510
+ else
511
+ raise
512
+ end
513
+ end
514
+
515
+ def stale_object_retry_delay(retry_count)
516
+ delay = DELAYS_PER_ATTEMPT[retry_count]
517
+ # Add jitter (±25% randomization) to prevent thundering herd
518
+ jitter = delay * 0.25
519
+ delay + (rand * 2 - 1) * jitter
520
+ end
489
521
  end
490
522
  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,21 @@ 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
+
36
+ # The frequency at which to reload the run status during iteration.
37
+ # Defaults to the global MaintenanceTasks.status_reload_frequency setting.
38
+ #
39
+ # @api private
40
+ class_attribute :status_reload_frequency, default: MaintenanceTasks.status_reload_frequency
41
+
30
42
  define_callbacks :start, :complete, :error, :cancel, :pause, :interrupt
31
43
 
44
+ attr_accessor :metadata
45
+
32
46
  class << self
33
47
  # Finds a Task with the given name.
34
48
  #
@@ -152,6 +166,22 @@ module MaintenanceTasks
152
166
  self.active_record_enumerator_batch_size = size
153
167
  end
154
168
 
169
+ # Adds attribute names to sensitive arguments list.
170
+ #
171
+ # @param attributes [Array<Symbol>] the attribute names to filter.
172
+ def mask_attribute(*attributes)
173
+ self.masked_arguments += attributes
174
+ end
175
+
176
+ # Configure how frequently the run status should be reloaded during iteration.
177
+ # Use this to reduce database queries when processing large collections.
178
+ #
179
+ # @param frequency [ActiveSupport::Duration, Numeric] reload status every N seconds (default: 1 second).
180
+ # Setting this to 10.seconds means status will be reloaded every 10 seconds.
181
+ def reload_status_every(frequency)
182
+ self.status_reload_frequency = frequency
183
+ end
184
+
155
185
  # Initialize a callback to run after the task starts.
156
186
  #
157
187
  # @param filter_list apply filters to the callback
@@ -200,6 +230,18 @@ module MaintenanceTasks
200
230
  set_callback(:error, :after, *filter_list, &block)
201
231
  end
202
232
 
233
+ # Rescue listed exceptions during an iteration and report them to the error reporter, then
234
+ # continue iteration.
235
+ #
236
+ # @param exceptions list of exceptions to rescue and report
237
+ # @param report_options [Hash] optionally, supply additional options for `Rails.error.report`.
238
+ # By default: <code>{ handled: true, source: "maintenance-tasks" }</code>.
239
+ def report_on(*exceptions, **report_options)
240
+ rescue_from(*exceptions) do |exception|
241
+ Rails.error.report(exception, source: "maintenance-tasks", **report_options)
242
+ end
243
+ end
244
+
203
245
  private
204
246
 
205
247
  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>
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <%= capybara_lockstep if defined?(Capybara::Lockstep) %>
6
7
 
7
8
  <title>
8
9
  <% if content_for?(:page_title) %>
@@ -15,10 +16,10 @@
15
16
  <%= csrf_meta_tags %>
16
17
 
17
18
  <%=
18
- stylesheet_link_tag(URI.join(controller.class::BULMA_CDN, 'npm/bulma@0.9.4/css/bulma.css'),
19
+ stylesheet_link_tag(URI.join(controller.class::BULMA_CDN, "npm/bulma@1.0.3/css/versions/bulma-no-dark-mode.min.css"),
19
20
  media: :all,
20
- integrity: 'sha384-qQlNh1kc0FyhUqUDXKkl5wpiiSm8PXQw2ZWhAVfU46tmdMDfq2vXG2CXWYT+Dls3',
21
- crossorigin: 'anonymous') unless request.xhr?
21
+ integrity: "sha256-HCNMQcqH/4MnGR0EYg2S3/BXYMM1z9lrFV10ANRd79o",
22
+ crossorigin: "anonymous") unless request.xhr?
22
23
  %>
23
24
 
24
25
  <style>
@@ -29,6 +30,36 @@
29
30
  .ruby-ivar, .ruby-cvar, .ruby-gvar, .ruby-int, .ruby-imaginary, .ruby-float, .ruby-rational { color: #005cc5; }
30
31
  .ruby-kw { color: #d73a49; }
31
32
  .ruby-label, .ruby-tstring-beg, .ruby-tstring-content, .ruby-tstring-end { color: #032f62; }
33
+
34
+ .select, select { width: 100%; }
35
+ summary { cursor: pointer; }
36
+ input[type="datetime-local"], input[type="date"], input[type="time"] {
37
+ width: fit-content;
38
+ }
39
+ details > summary {
40
+ list-style: none;
41
+ }
42
+ summary::-webkit-details-marker {
43
+ display: none
44
+ }
45
+ summary::before {
46
+ content: '► ';
47
+ position:absolute;
48
+ font-size: 16px
49
+ }
50
+ details[open] summary:before {
51
+ content: "▼ ";
52
+ }
53
+
54
+ .box {
55
+ box-shadow: 0 4px 6px -1px #0000001a,
56
+ 0 2px 4px -2px #0000001a;
57
+ }
58
+ .label.is-required:after {
59
+ content: " (required)";
60
+ color: #ff6685;
61
+ font-size: 12px;
62
+ }
32
63
  </style>
33
64
 
34
65
  <script>
@@ -69,6 +100,6 @@
69
100
 
70
101
  <%= yield %>
71
102
  </div>
72
- </div>
103
+ </section>
73
104
  </body>
74
105
  </html>
@@ -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
 
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateMaintenanceTasksRuns < ActiveRecord::Migration[6.0]
3
+ class CreateMaintenanceTasksRuns < ActiveRecord::Migration[7.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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ChangeCursorToString < ActiveRecord::Migration[6.0]
3
+ class ChangeCursorToString < ActiveRecord::Migration[7.0]
4
4
  # This migration will clear all existing data in the cursor column with MySQL.
5
5
  # Ensure no Tasks are paused when this migration is deployed, or they will be resumed from the start.
6
6
  # Running tasks are able to gracefully handle this change, even if interrupted.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class RemoveIndexOnTaskName < ActiveRecord::Migration[6.0]
3
+ class RemoveIndexOnTaskName < ActiveRecord::Migration[7.0]
4
4
  def up
5
5
  change_table(:maintenance_tasks_runs) do |t|
6
6
  t.remove_index(:task_name)