solid_job_monitor 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 (147) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +106 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/mission_control_jobs_manifest.js +4 -0
  6. data/app/assets/stylesheets/mission_control/jobs/application.css +16 -0
  7. data/app/assets/stylesheets/mission_control/jobs/bulma.min.css +21013 -0
  8. data/app/assets/stylesheets/mission_control/jobs/forms.css +8 -0
  9. data/app/assets/stylesheets/mission_control/jobs/jobs.css +68 -0
  10. data/app/controllers/concerns/mission_control/jobs/adapter_features.rb +24 -0
  11. data/app/controllers/concerns/mission_control/jobs/application_scoped.rb +38 -0
  12. data/app/controllers/concerns/mission_control/jobs/basic_authentication.rb +33 -0
  13. data/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb +17 -0
  14. data/app/controllers/concerns/mission_control/jobs/job_filters.rb +41 -0
  15. data/app/controllers/concerns/mission_control/jobs/job_scoped.rb +16 -0
  16. data/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb +36 -0
  17. data/app/controllers/concerns/mission_control/jobs/queue_scoped.rb +12 -0
  18. data/app/controllers/mission_control/jobs/application_controller.rb +32 -0
  19. data/app/controllers/mission_control/jobs/bulk_discards_controller.rb +20 -0
  20. data/app/controllers/mission_control/jobs/bulk_retries_controller.rb +10 -0
  21. data/app/controllers/mission_control/jobs/discards_controller.rb +18 -0
  22. data/app/controllers/mission_control/jobs/dispatches_controller.rb +18 -0
  23. data/app/controllers/mission_control/jobs/jobs_controller.rb +54 -0
  24. data/app/controllers/mission_control/jobs/progresses_controller.rb +27 -0
  25. data/app/controllers/mission_control/jobs/queues/pauses_controller.rb +15 -0
  26. data/app/controllers/mission_control/jobs/queues_controller.rb +16 -0
  27. data/app/controllers/mission_control/jobs/recurring_tasks_controller.rb +38 -0
  28. data/app/controllers/mission_control/jobs/retries_controller.rb +13 -0
  29. data/app/controllers/mission_control/jobs/stops_controller.rb +22 -0
  30. data/app/controllers/mission_control/jobs/workers_controller.rb +23 -0
  31. data/app/helpers/mission_control/jobs/application_helper.rb +9 -0
  32. data/app/helpers/mission_control/jobs/dates_helper.rb +19 -0
  33. data/app/helpers/mission_control/jobs/interface_helper.rb +23 -0
  34. data/app/helpers/mission_control/jobs/jobs_helper.rb +94 -0
  35. data/app/helpers/mission_control/jobs/navigation_helper.rb +60 -0
  36. data/app/javascript/mission_control/jobs/application.js +4 -0
  37. data/app/javascript/mission_control/jobs/controllers/application.js +9 -0
  38. data/app/javascript/mission_control/jobs/controllers/form_controller.js +21 -0
  39. data/app/javascript/mission_control/jobs/controllers/index.js +11 -0
  40. data/app/javascript/mission_control/jobs/controllers/progress_controller.js +55 -0
  41. data/app/javascript/mission_control/jobs/helpers/debounce_helpers.js +9 -0
  42. data/app/javascript/mission_control/jobs/helpers/index.js +1 -0
  43. data/app/models/mission_control/jobs/application_record.rb +7 -0
  44. data/app/models/mission_control/jobs/current.rb +3 -0
  45. data/app/models/mission_control/jobs/job_progress.rb +7 -0
  46. data/app/models/mission_control/jobs/page.rb +48 -0
  47. data/app/models/mission_control/jobs/recurring_task.rb +25 -0
  48. data/app/models/mission_control/jobs/worker.rb +17 -0
  49. data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +14 -0
  50. data/app/views/layouts/mission_control/jobs/_flash.html.erb +9 -0
  51. data/app/views/layouts/mission_control/jobs/_navigation.html.erb +9 -0
  52. data/app/views/layouts/mission_control/jobs/application.html.erb +27 -0
  53. data/app/views/layouts/mission_control/jobs/application_selection/_applications.html.erb +13 -0
  54. data/app/views/layouts/mission_control/jobs/application_selection/_servers.html.erb +15 -0
  55. data/app/views/mission_control/jobs/jobs/_error_information.html.erb +22 -0
  56. data/app/views/mission_control/jobs/jobs/_filters.html.erb +55 -0
  57. data/app/views/mission_control/jobs/jobs/_general_information.html.erb +85 -0
  58. data/app/views/mission_control/jobs/jobs/_job.html.erb +13 -0
  59. data/app/views/mission_control/jobs/jobs/_jobs_page.html.erb +13 -0
  60. data/app/views/mission_control/jobs/jobs/_progress_bar.html.erb +16 -0
  61. data/app/views/mission_control/jobs/jobs/_raw_data.html.erb +4 -0
  62. data/app/views/mission_control/jobs/jobs/_title.html.erb +22 -0
  63. data/app/views/mission_control/jobs/jobs/_toolbar.html.erb +18 -0
  64. data/app/views/mission_control/jobs/jobs/blocked/_actions.html.erb +3 -0
  65. data/app/views/mission_control/jobs/jobs/blocked/_job.html.erb +8 -0
  66. data/app/views/mission_control/jobs/jobs/failed/_actions.html.erb +5 -0
  67. data/app/views/mission_control/jobs/jobs/failed/_backtrace_toggle.html.erb +14 -0
  68. data/app/views/mission_control/jobs/jobs/failed/_job.html.erb +7 -0
  69. data/app/views/mission_control/jobs/jobs/finished/_job.html.erb +2 -0
  70. data/app/views/mission_control/jobs/jobs/in_progress/_actions.html.erb +6 -0
  71. data/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb +13 -0
  72. data/app/views/mission_control/jobs/jobs/index.html.erb +19 -0
  73. data/app/views/mission_control/jobs/jobs/scheduled/_actions.html.erb +5 -0
  74. data/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb +13 -0
  75. data/app/views/mission_control/jobs/jobs/show.html.erb +6 -0
  76. data/app/views/mission_control/jobs/queues/_actions.html.erb +7 -0
  77. data/app/views/mission_control/jobs/queues/_job.html.erb +15 -0
  78. data/app/views/mission_control/jobs/queues/_queue.html.erb +16 -0
  79. data/app/views/mission_control/jobs/queues/_queue_title.html.erb +17 -0
  80. data/app/views/mission_control/jobs/queues/index.html.erb +16 -0
  81. data/app/views/mission_control/jobs/queues/show.html.erb +25 -0
  82. data/app/views/mission_control/jobs/recurring_tasks/_actions.html.erb +5 -0
  83. data/app/views/mission_control/jobs/recurring_tasks/_general_information.html.erb +37 -0
  84. data/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb +22 -0
  85. data/app/views/mission_control/jobs/recurring_tasks/_title.html.erb +10 -0
  86. data/app/views/mission_control/jobs/recurring_tasks/index.html.erb +18 -0
  87. data/app/views/mission_control/jobs/recurring_tasks/show.html.erb +14 -0
  88. data/app/views/mission_control/jobs/shared/_job.html.erb +27 -0
  89. data/app/views/mission_control/jobs/shared/_jobs.html.erb +14 -0
  90. data/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb +7 -0
  91. data/app/views/mission_control/jobs/workers/_configuration.html.erb +6 -0
  92. data/app/views/mission_control/jobs/workers/_raw_data.html.erb +6 -0
  93. data/app/views/mission_control/jobs/workers/_title.html.erb +11 -0
  94. data/app/views/mission_control/jobs/workers/_worker.html.erb +21 -0
  95. data/app/views/mission_control/jobs/workers/_workers_page.html.erb +15 -0
  96. data/app/views/mission_control/jobs/workers/index.html.erb +6 -0
  97. data/app/views/mission_control/jobs/workers/show.html.erb +14 -0
  98. data/config/importmap.rb +6 -0
  99. data/config/routes.rb +35 -0
  100. data/db/migrate/20260828105144_create_mission_control_job_progresses.rb +16 -0
  101. data/lib/active_job/errors/invalid_operation.rb +5 -0
  102. data/lib/active_job/errors/job_not_found_error.rb +14 -0
  103. data/lib/active_job/errors/query_error.rb +5 -0
  104. data/lib/active_job/executing.rb +40 -0
  105. data/lib/active_job/execution_error.rb +8 -0
  106. data/lib/active_job/failed.rb +7 -0
  107. data/lib/active_job/job_proxy.rb +38 -0
  108. data/lib/active_job/jobs_relation.rb +318 -0
  109. data/lib/active_job/progress_reporting.rb +21 -0
  110. data/lib/active_job/querying.rb +44 -0
  111. data/lib/active_job/queue.rb +62 -0
  112. data/lib/active_job/queue_adapters/async_ext.rb +49 -0
  113. data/lib/active_job/queue_adapters/resque_ext.rb +317 -0
  114. data/lib/active_job/queue_adapters/solid_queue_ext/recurring_tasks.rb +52 -0
  115. data/lib/active_job/queue_adapters/solid_queue_ext/workers.rb +41 -0
  116. data/lib/active_job/queue_adapters/solid_queue_ext.rb +313 -0
  117. data/lib/active_job/queues.rb +33 -0
  118. data/lib/mission_control/jobs/adapter.rb +156 -0
  119. data/lib/mission_control/jobs/application.rb +20 -0
  120. data/lib/mission_control/jobs/applications.rb +8 -0
  121. data/lib/mission_control/jobs/arguments_filter.rb +24 -0
  122. data/lib/mission_control/jobs/authentication.rb +67 -0
  123. data/lib/mission_control/jobs/console/connect_to.rb +15 -0
  124. data/lib/mission_control/jobs/console/context.rb +11 -0
  125. data/lib/mission_control/jobs/console/jobs_help.rb +23 -0
  126. data/lib/mission_control/jobs/engine.rb +118 -0
  127. data/lib/mission_control/jobs/errors/incompatible_adapter.rb +2 -0
  128. data/lib/mission_control/jobs/errors/resource_not_found.rb +2 -0
  129. data/lib/mission_control/jobs/i18n_config.rb +17 -0
  130. data/lib/mission_control/jobs/identified_by_name.rb +18 -0
  131. data/lib/mission_control/jobs/identified_elements.rb +23 -0
  132. data/lib/mission_control/jobs/job_stopped_error.rb +5 -0
  133. data/lib/mission_control/jobs/progress/null_store.rb +25 -0
  134. data/lib/mission_control/jobs/progress/reporter.rb +63 -0
  135. data/lib/mission_control/jobs/progress/store.rb +72 -0
  136. data/lib/mission_control/jobs/progress.rb +17 -0
  137. data/lib/mission_control/jobs/server/recurring_tasks.rb +15 -0
  138. data/lib/mission_control/jobs/server/serializable.rb +24 -0
  139. data/lib/mission_control/jobs/server/workers.rb +13 -0
  140. data/lib/mission_control/jobs/server.rb +27 -0
  141. data/lib/mission_control/jobs/tasks.rb +8 -0
  142. data/lib/mission_control/jobs/version.rb +5 -0
  143. data/lib/mission_control/jobs/workers_relation.rb +78 -0
  144. data/lib/mission_control/jobs.rb +43 -0
  145. data/lib/resque/thread_safe_redis.rb +34 -0
  146. data/lib/solid_job_monitor.rb +6 -0
  147. metadata +527 -0
@@ -0,0 +1,94 @@
1
+ module MissionControl::Jobs::JobsHelper
2
+ def job_title(job)
3
+ job.job_class_name
4
+ end
5
+
6
+ def job_arguments(job)
7
+ renderable_job_arguments_for(job).join(", ")
8
+ end
9
+
10
+ def failed_job_error(job)
11
+ "#{job.last_execution_error.error_class}: #{job.last_execution_error.message}"
12
+ end
13
+
14
+ def clean_backtrace?
15
+ params["clean_backtrace"] == "true"
16
+ end
17
+
18
+ def failed_job_backtrace(job, server)
19
+ if clean_backtrace? && server&.backtrace_cleaner
20
+ server.backtrace_cleaner.clean(job.last_execution_error.backtrace).join("\n")
21
+ else
22
+ job.last_execution_error.backtrace.join("\n")
23
+ end
24
+ end
25
+
26
+ def attribute_names_for_job_status(status)
27
+ case status.to_s
28
+ when "failed" then [ "Error", "" ]
29
+ when "blocked" then [ "Queue", "Blocked by", "" ]
30
+ when "finished" then [ "Queue", "Finished" ]
31
+ when "scheduled" then [ "Queue", "Scheduled", "" ]
32
+ when "in_progress" then [ "Queue", "Run by", "Running for", "Progress", "" ]
33
+ else []
34
+ end
35
+ end
36
+
37
+ def job_delayed?(job)
38
+ job.scheduled_at.before?(MissionControl::Jobs.scheduled_job_delay_threshold.ago)
39
+ end
40
+
41
+ def blocked_job_expiration(job)
42
+ "Expires #{bidirectional_time_distance_in_words_with_title(job.blocked_until)}".html_safe if job.blocked_until
43
+ end
44
+
45
+ def job_progress_enabled?
46
+ MissionControl::Jobs::Progress.available?
47
+ end
48
+
49
+ def progress_for(job)
50
+ @progress_by_job_id&.[](job.job_id) || @progress
51
+ end
52
+
53
+ private
54
+ def renderable_job_arguments_for(job)
55
+ job.serialized_arguments.collect do |argument|
56
+ as_renderable_argument(argument)
57
+ end
58
+ end
59
+
60
+ def as_renderable_argument(argument)
61
+ case argument
62
+ when Hash
63
+ as_renderable_hash(argument)
64
+ when Array
65
+ as_renderable_array(argument)
66
+ else
67
+ ActiveJob::Arguments.deserialize([ argument ]).first
68
+ end
69
+ rescue ActiveJob::DeserializationError
70
+ argument.to_s
71
+ end
72
+
73
+ def as_renderable_hash(argument)
74
+ if argument["_aj_globalid"]
75
+ # don't deserialize as the class might not exist in the host app running the engine
76
+ argument["_aj_globalid"]
77
+ elsif argument["_aj_serialized"] == "ActiveJob::Serializers::ModuleSerializer"
78
+ argument["value"]
79
+ elsif argument["_aj_serialized"]
80
+ ActiveJob::Arguments.deserialize([ argument ]).first
81
+ else
82
+ MissionControl::Jobs.job_arguments_filter.apply_to(argument)
83
+ .without("_aj_symbol_keys", "_aj_ruby2_keywords")
84
+ .transform_values { |v| as_renderable_argument(v) }
85
+ .map { |k, v| "#{k}: #{v}" }
86
+ .join(", ")
87
+ .then { |s| "{#{s}}" }
88
+ end
89
+ end
90
+
91
+ def as_renderable_array(argument)
92
+ "[#{argument.collect { |part| as_renderable_argument(part) }.join(", ")}]"
93
+ end
94
+ end
@@ -0,0 +1,60 @@
1
+ module MissionControl::Jobs::NavigationHelper
2
+ attr_reader :page_title, :current_section
3
+
4
+ def back_to_main_app_path
5
+ MissionControl::Jobs.back_to_main_app_path.presence || main_app.try(:root_path)
6
+ end
7
+
8
+ def navigation_sections
9
+ { queues: [ "Queues", application_queues_path(@application) ] }.tap do |sections|
10
+ supported_job_statuses.without(:pending).each do |status|
11
+ sections[navigation_section_for_status(status)] = [ "#{status.to_s.titleize} jobs (#{jobs_count_with_status(status)})", application_jobs_path(@application, status) ]
12
+ end
13
+
14
+ sections[:workers] = [ "Workers", application_workers_path(@application) ] if workers_exposed?
15
+ sections[:recurring_tasks] = [ "Recurring tasks", application_recurring_tasks_path(@application) ] if recurring_tasks_supported?
16
+ end
17
+ end
18
+
19
+ def navigation_section_for_status(status)
20
+ if status.nil? || status == :pending
21
+ :queues
22
+ else
23
+ "#{status}_jobs".to_sym
24
+ end
25
+ end
26
+
27
+ def navigation(title: nil, section: nil)
28
+ @page_title = title
29
+ @current_section = section
30
+ end
31
+
32
+ def selected_application?(application)
33
+ MissionControl::Jobs::Current.application.name == application.name
34
+ end
35
+
36
+ def selectable_applications
37
+ MissionControl::Jobs.applications.reject { |app| selected_application?(app) }
38
+ end
39
+
40
+ def selected_server?(server)
41
+ MissionControl::Jobs::Current.server.name == server.name
42
+ end
43
+
44
+ def jobs_count_with_status(status)
45
+ count = ActiveJob.jobs.with_status(status).count
46
+ if count.infinite?
47
+ "..."
48
+ else
49
+ number_to_human(count,
50
+ format: "%n%u",
51
+ units: {
52
+ thousand: "K",
53
+ million: "M",
54
+ billion: "B",
55
+ trillion: "T",
56
+ quadrillion: "Q"
57
+ })
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,4 @@
1
+ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2
+ import "@hotwired/turbo-rails"
3
+ import "controllers"
4
+ import "helpers"
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+
3
+ const application = Application.start()
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false
7
+ window.Stimulus = application
8
+
9
+ export { application }
@@ -0,0 +1,21 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { debounce } from "helpers"
3
+
4
+ export default class extends Controller {
5
+ static values = {
6
+ debounceTimeout: { type: Number, default: 300 }
7
+ }
8
+
9
+ initialize() {
10
+ this.debouncedSubmit = debounce(this.debouncedSubmit.bind(this), this.debounceTimeoutValue)
11
+ }
12
+
13
+ submit(event) {
14
+ const form = event.target.form || event.target.closest("form")
15
+ if (form) form.requestSubmit()
16
+ }
17
+
18
+ debouncedSubmit(event) {
19
+ this.submit(event)
20
+ }
21
+ }
@@ -0,0 +1,11 @@
1
+ // Import and register all your controllers from the importmap under controllers/*
2
+
3
+ import { application } from "controllers/application"
4
+
5
+ // Eager load all controllers defined in the import map under controllers/**/*_controller
6
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
7
+ eagerLoadControllersFrom("controllers", application)
8
+
9
+ // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
10
+ // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
11
+ // lazyLoadControllersFrom("controllers", application)
@@ -0,0 +1,55 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = [ "bar", "label", "stopping" ]
5
+ static values = {
6
+ url: String,
7
+ percent: { type: Number, default: 0 },
8
+ interval: { type: Number, default: 2000 }
9
+ }
10
+
11
+ connect() {
12
+ this.apply({ percent: this.percentValue, stop_requested: this.hasStoppingTarget && !this.stoppingTarget.classList.contains("is-hidden") })
13
+ this.timer = setInterval(() => this.refresh(), this.intervalValue)
14
+ }
15
+
16
+ disconnect() {
17
+ if (this.timer) clearInterval(this.timer)
18
+ }
19
+
20
+ async refresh() {
21
+ try {
22
+ const response = await fetch(this.urlValue, {
23
+ headers: { Accept: "application/json" }
24
+ })
25
+
26
+ if (!response.ok) {
27
+ this.disconnect()
28
+ return
29
+ }
30
+
31
+ const data = await response.json()
32
+ if (data.missing) {
33
+ this.disconnect()
34
+ return
35
+ }
36
+
37
+ this.apply(data)
38
+
39
+ if (data.percent === 100) this.disconnect()
40
+ } catch (_error) {
41
+ this.disconnect()
42
+ }
43
+ }
44
+
45
+ apply(data) {
46
+ const percent = Number.isFinite(data.percent) ? data.percent : 0
47
+
48
+ if (this.hasBarTarget) this.barTarget.style.width = `${percent}%`
49
+ if (this.hasLabelTarget) this.labelTarget.textContent = `${percent}%`
50
+
51
+ if (this.hasStoppingTarget) {
52
+ this.stoppingTarget.classList.toggle("is-hidden", !data.stop_requested)
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,9 @@
1
+ export function debounce(fn, delay = 10) {
2
+ let timeoutId = null
3
+
4
+ return (...args) => {
5
+ const callback = () => fn.apply(this, args)
6
+ clearTimeout(timeoutId)
7
+ timeoutId = setTimeout(callback, delay)
8
+ }
9
+ }
@@ -0,0 +1 @@
1
+ export * from "helpers/debounce_helpers"
@@ -0,0 +1,7 @@
1
+ module MissionControl
2
+ module Jobs
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ class MissionControl::Jobs::Current < ActiveSupport::CurrentAttributes
2
+ attribute :application, :server
3
+ end
@@ -0,0 +1,7 @@
1
+ module MissionControl
2
+ module Jobs
3
+ class JobProgress < ApplicationRecord
4
+ self.table_name = "mission_control_job_progresses"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,48 @@
1
+ class MissionControl::Jobs::Page
2
+ DEFAULT_PAGE_SIZE = 10
3
+
4
+ attr_reader :records, :index, :page_size
5
+
6
+ def initialize(relation, page: 1, page_size: DEFAULT_PAGE_SIZE)
7
+ @relation = relation
8
+ @page_size = page_size
9
+ @index = [ page, 1 ].max
10
+ end
11
+
12
+ def records
13
+ @relation.limit(page_size).offset(offset)
14
+ end
15
+
16
+ def first?
17
+ index == 1
18
+ end
19
+
20
+ def last?
21
+ index == pages_count || empty? || records.empty?
22
+ end
23
+
24
+ def empty?
25
+ total_count == 0
26
+ end
27
+
28
+ def previous_index
29
+ [ index - 1, 1 ].max
30
+ end
31
+
32
+ def next_index
33
+ pages_count ? [ index + 1, pages_count ].min : index + 1
34
+ end
35
+
36
+ def pages_count
37
+ (total_count.to_f / page_size).ceil unless total_count.infinite?
38
+ end
39
+
40
+ def total_count
41
+ @total_count ||= @relation.count # Potentially expensive when filtering a lot of records, with the adapter in charge of doing the filtering in memory
42
+ end
43
+
44
+ private
45
+ def offset
46
+ (index - 1) * page_size
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ class MissionControl::Jobs::RecurringTask
2
+ include ActiveModel::Model
3
+
4
+ attr_accessor :id, :job_class_name, :command, :arguments, :schedule, :last_enqueued_at, :next_time, :queue_name, :priority
5
+
6
+ def initialize(queue_adapter: ActiveJob::Base.queue_adapter, **kwargs)
7
+ @queue_adapter = queue_adapter
8
+ super(**kwargs)
9
+ end
10
+
11
+ def jobs
12
+ ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).where(recurring_task_id: id)
13
+ end
14
+
15
+ def enqueue
16
+ queue_adapter.enqueue_recurring_task(id)
17
+ end
18
+
19
+ def runnable?
20
+ queue_adapter.can_enqueue_recurring_task?(id)
21
+ end
22
+
23
+ private
24
+ attr_reader :queue_adapter
25
+ end
@@ -0,0 +1,17 @@
1
+ class MissionControl::Jobs::Worker
2
+ include ActiveModel::Model
3
+
4
+ attr_accessor :id, :name, :hostname, :last_heartbeat_at, :configuration, :raw_data
5
+
6
+ def initialize(queue_adapter: ActiveJob::Base.queue_adapter, **kwargs)
7
+ @queue_adapter = queue_adapter
8
+ super(**kwargs)
9
+ end
10
+
11
+ def jobs
12
+ @jobs ||= ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).in_progress.where(worker_id: id)
13
+ end
14
+
15
+ private
16
+ attr_reader :queue_adapter
17
+ end
@@ -0,0 +1,14 @@
1
+ <nav class="navbar" role="navigation" aria-label="main navigation">
2
+ <div class="navbar-menu is-active mb-4">
3
+ <div class="navbar-start">
4
+ <% if (path = back_to_main_app_path) %>
5
+ <%= link_to "Back to main app", path %>
6
+ <% end %>
7
+ </div>
8
+
9
+ <div class="navbar-end">
10
+ <%= render "layouts/mission_control/jobs/application_selection/servers", application: @application %>
11
+ <%= render "layouts/mission_control/jobs/application_selection/applications" if selectable_applications.any? %>
12
+ </div>
13
+ </div>
14
+ </nav>
@@ -0,0 +1,9 @@
1
+ <% flash.each do |name, message| -%>
2
+ <% message_class = { notice: "is-success", alert: "is-danger" }[name.to_sym] %>
3
+
4
+ <article class="message <%= message_class %>">
5
+ <div class="message-body">
6
+ <%= message %>
7
+ </div>
8
+ </article>
9
+ <% end -%>
@@ -0,0 +1,9 @@
1
+ <div class="tabs is-boxed">
2
+ <ul>
3
+ <% navigation_sections.each do |key, (label, url)| %>
4
+ <li class="<%= "is-active" if key == current_section %>">
5
+ <%= link_to label, url %>
6
+ </li>
7
+ <% end %>
8
+ </ul>
9
+ </div>
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Mission control - <%= page_title %></title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <meta name="viewport" content="width=device-width,initial-scale=1">
9
+ <meta name="turbo-cache-control" content="no-cache">
10
+
11
+ <%= stylesheet_link_tag "mission_control/jobs/bulma.min" %>
12
+ <%= stylesheet_link_tag "mission_control/jobs/application", "data-turbo-track": "reload" %>
13
+ <%= javascript_importmap_tags "application", importmap: MissionControl::Jobs.importmap %>
14
+ </head>
15
+ <body>
16
+
17
+ <section class="section">
18
+ <div class="container">
19
+ <%= render "layouts/mission_control/jobs/application_selection" %>
20
+ <%= render "layouts/mission_control/jobs/flash" %>
21
+ <%= render "layouts/mission_control/jobs/navigation" %>
22
+ <%= yield %>
23
+ </div>
24
+ </section>
25
+
26
+ </body>
27
+ </html>
@@ -0,0 +1,13 @@
1
+ <div class="application-selector navbar-item has-dropdown is-hoverable">
2
+ <a class="navbar-link">
3
+ <%= MissionControl::Jobs::Current.application.name %>
4
+ </a>
5
+
6
+ <div class="navbar-dropdown">
7
+ <% selectable_applications.each do |application| %>
8
+ <%= link_to application_queues_path(application, server_id: nil), class: "navbar-item" do %>
9
+ <span><%= application.name %></span>
10
+ <% end %>
11
+ <% end %>
12
+ </div>
13
+ </div>
@@ -0,0 +1,15 @@
1
+ <% if application.servers.many? %>
2
+ <div class="server-selector navbar-item">
3
+ <div class="tabs is-toggle is-toggle-rounded is-small">
4
+ <ul>
5
+ <% application.servers.each do |server| %>
6
+ <li class="<%= "is-active" if selected_server?(server) %>">
7
+ <%= link_to application_queues_path(application, server_id: server) do %>
8
+ <span><%= server.name %></span>
9
+ <% end %>
10
+ </li>
11
+ <% end %>
12
+ </ul>
13
+ </div>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <% if job.failed? %>
2
+ <h2 class="subtitle">Error information</h2>
3
+
4
+ <table class="table">
5
+ <tbody>
6
+ <tr>
7
+ <th>Type</th>
8
+ <td> <%= job.last_execution_error.error_class %> </td>
9
+ </tr>
10
+ <tr>
11
+ <th>Message</th>
12
+ <td> <%= job.last_execution_error.try(:message) || job.last_execution_error.inspect %> </td>
13
+ </tr>
14
+ </tbody>
15
+ </table>
16
+
17
+ <% if @server.backtrace_cleaner %>
18
+ <%= render "mission_control/jobs/jobs/failed/backtrace_toggle", application: @application, job: job %>
19
+ <% end %>
20
+
21
+ <pre class="is-family-monospace mb-4 backtrace-content"><%= failed_job_backtrace(job, @server) %></pre>
22
+ <% end %>
@@ -0,0 +1,55 @@
1
+ <div class="filter level-left">
2
+ <%= form_for :filter, url: application_jobs_path(MissionControl::Jobs::Current.application, jobs_status), method: :get,
3
+ data: { controller: "form", action: "input->form#debouncedSubmit" } do |form| %>
4
+
5
+ <div class="field is-grouped">
6
+ <div class="control">
7
+ <%= form.label :job_class_name, class: "label" %>
8
+ <div class="select is-rounded">
9
+ <%= form.text_field :job_class_name, value: @job_filters[:job_class_name], class: "input", list: "job-classes", placeholder: "Filter by job class...", autocomplete: "off" %>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="control">
14
+ <%= form.label :queue_name, class: "label" %>
15
+ <div class="select is-rounded">
16
+ <%= form.text_field :queue_name, value: @job_filters[:queue_name], class: "input", list: "queue-names", placeholder: "Filter by queue name...", autocomplete: "off" %>
17
+ </div>
18
+ </div>
19
+
20
+ <% if jobs_status == "finished" %>
21
+ <div class="control">
22
+ <%= form.label :finished_at_start, class: "label" %>
23
+ <div class="select is-rounded">
24
+ <%= form.datetime_field :finished_at_start, value: @job_filters[:finished_at]&.begin, class: "input", placeholder: "Finished from" %>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="control">
29
+ <%= form.label :finished_at_end, class: "label" %>
30
+ <div class="select is-rounded">
31
+ <%= form.datetime_field :finished_at_end, value: @job_filters[:finished_at]&.end, class: "input", placeholder: "Finished to" %>
32
+ </div>
33
+ </div>
34
+ <% end %>
35
+
36
+ <%= hidden_field_tag :server_id, MissionControl::Jobs::Current.server.id %>
37
+
38
+ <datalist id="job-classes" class="is-hidden">
39
+ <% job_class_names.each do |job_class_name| %>
40
+ <option value="<%= job_class_name %>"></option>
41
+ <% end %>
42
+ </datalist>
43
+
44
+ <datalist id="queue-names" class="is-hidden">
45
+ <% queue_names.each do |queue_name| %>
46
+ <option value="<%= queue_name %>"></option>
47
+ <% end %>
48
+ </datalist>
49
+
50
+ <div class="control is-align-self-flex-end">
51
+ <%= 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" %>
52
+ </div>
53
+ </div>
54
+ <% end %>
55
+ </div>
@@ -0,0 +1,85 @@
1
+ <table class="table">
2
+ <tbody>
3
+ <tr>
4
+ <th>Arguments</th>
5
+ <td>
6
+ <div class="is-family-monospace">
7
+ <%= job_arguments(job) %>
8
+ </div>
9
+ </td>
10
+ </tr>
11
+ <tr>
12
+ <th>Job id</th>
13
+ <td><%= job.job_id %></td>
14
+ </tr>
15
+ <tr>
16
+ <th>Queue</th>
17
+ <td>
18
+ <div class="tag">
19
+ <%= link_to job.queue_name, application_queue_path(@application, job.queue) %>
20
+ </div>
21
+ </td>
22
+ </tr>
23
+ <tr>
24
+ <th>Enqueued</th>
25
+ <td>
26
+ <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
27
+ </td>
28
+ </tr>
29
+ <% if job.executions > 0 %>
30
+ <tr>
31
+ <th>Retries</th>
32
+ <td><%= job.executions %></td>
33
+ </tr>
34
+ <% end %>
35
+ <% if job.scheduled? %>
36
+ <tr>
37
+ <th>Scheduled</th>
38
+ <td>
39
+ <%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %>
40
+ <% if job_delayed?(job) %>
41
+ <div class="is-danger tag ml-4">delayed</div>
42
+ <% end %>
43
+ </td>
44
+ </tr>
45
+ <% end %>
46
+ <% if job.failed? %>
47
+ <tr>
48
+ <th>Failed</th>
49
+ <td>
50
+ <%= time_distance_in_words_with_title(job.failed_at) %> ago
51
+ </td>
52
+ </tr>
53
+ <% end %>
54
+ <% if job.finished_at.present? %>
55
+ <tr>
56
+ <th>Finished</th>
57
+ <td>
58
+ <%= time_distance_in_words_with_title(job.finished_at) %> ago
59
+ </td>
60
+ </tr>
61
+ <tr>
62
+ <th>Duration</th>
63
+ <td>
64
+ <%= job.duration.round(3) %> seconds
65
+ </td>
66
+ </tr>
67
+ <% end %>
68
+ <% if job.worker_id.present? %>
69
+ <tr>
70
+ <th>Processed by</th>
71
+ <td>
72
+ <%= link_to "worker #{job.worker_id}", application_worker_path(@application, job.worker_id) %>
73
+ </td>
74
+ </tr>
75
+ <% end %>
76
+ <% if job.in_progress? || progress_for(job) %>
77
+ <tr>
78
+ <th>Progress</th>
79
+ <td>
80
+ <%= render "mission_control/jobs/jobs/progress_bar", progress: progress_for(job), job: job %>
81
+ </td>
82
+ </tr>
83
+ <% end %>
84
+ </tbody>
85
+ </table>
@@ -0,0 +1,13 @@
1
+ <tr class="job">
2
+ <td>
3
+ <%= link_to job_title(job), application_job_path(@application, job.job_id) %>
4
+
5
+ <% if job.serialized_arguments.present? %>
6
+ <div class="is-family-monospace"><%= job_arguments(job) %></div>
7
+ <% end %>
8
+
9
+ <div class="has-text-grey is-size-7">Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago</div>
10
+ </td>
11
+
12
+ <%= render "mission_control/jobs/jobs/#{jobs_status}/job", job: job %>
13
+ </tr>