flight_control 2.0.0.beta.1 → 2.0.0.beta.3

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -1
  3. data/README.md +23 -0
  4. data/app/assets/config/flight_control_manifest.js +3 -0
  5. data/app/assets/stylesheets/flight_control/application.css +5 -0
  6. data/app/assets/stylesheets/flight_control/bulma.min.css +21013 -0
  7. data/app/assets/stylesheets/flight_control/forms.css +8 -0
  8. data/app/assets/stylesheets/flight_control/jobs.css +42 -0
  9. data/app/controllers/concerns/flight_control/adapter_features.rb +25 -0
  10. data/app/controllers/concerns/flight_control/application_scoped.rb +39 -0
  11. data/app/controllers/concerns/flight_control/basic_authentication.rb +34 -0
  12. data/app/controllers/concerns/flight_control/failed_jobs_bulk_operations.rb +18 -0
  13. data/app/controllers/concerns/flight_control/job_filters.rb +44 -0
  14. data/app/controllers/concerns/flight_control/job_scoped.rb +17 -0
  15. data/app/controllers/concerns/flight_control/not_found_redirections.rb +36 -0
  16. data/app/controllers/concerns/flight_control/queue_scoped.rb +13 -0
  17. data/app/controllers/flight_control/application_controller.rb +35 -0
  18. data/app/controllers/flight_control/bulk_discards_controller.rb +21 -0
  19. data/app/controllers/flight_control/bulk_retries_controller.rb +10 -0
  20. data/app/controllers/flight_control/discards_controller.rb +19 -0
  21. data/app/controllers/flight_control/dispatches_controller.rb +19 -0
  22. data/app/controllers/flight_control/job_launches_controller.rb +28 -0
  23. data/app/controllers/flight_control/jobs_controller.rb +40 -0
  24. data/app/controllers/flight_control/queues/pauses_controller.rb +15 -0
  25. data/app/controllers/flight_control/queues_controller.rb +17 -0
  26. data/app/controllers/flight_control/recurring_tasks_controller.rb +39 -0
  27. data/app/controllers/flight_control/retries_controller.rb +14 -0
  28. data/app/controllers/flight_control/workers_controller.rb +24 -0
  29. data/app/helpers/flight_control/application_helper.rb +12 -0
  30. data/app/helpers/flight_control/dates_helper.rb +30 -0
  31. data/app/helpers/flight_control/interface_helper.rb +23 -0
  32. data/app/helpers/flight_control/jobs_helper.rb +105 -0
  33. data/app/helpers/flight_control/navigation_helper.rb +61 -0
  34. data/app/javascript/flight_control/application.js +3 -0
  35. data/app/javascript/flight_control/controllers/application.js +9 -0
  36. data/app/javascript/flight_control/controllers/form_controller.js +8 -0
  37. data/app/javascript/flight_control/controllers/index.js +11 -0
  38. data/app/javascript/flight_control/controllers/local_time_controller.js +27 -0
  39. data/app/models/flight_control/application_record.rb +5 -0
  40. data/app/models/flight_control/current.rb +3 -0
  41. data/app/models/flight_control/job_launch.rb +75 -0
  42. data/app/models/flight_control/page.rb +49 -0
  43. data/app/models/flight_control/recurring_task.rb +26 -0
  44. data/app/models/flight_control/worker.rb +18 -0
  45. data/app/views/flight_control/job_launches/new.html.erb +53 -0
  46. data/app/views/flight_control/jobs/_date_range_filter.html.erb +11 -0
  47. data/app/views/flight_control/jobs/_error_information.html.erb +24 -0
  48. data/app/views/flight_control/jobs/_filters.html.erb +49 -0
  49. data/app/views/flight_control/jobs/_general_information.html.erb +77 -0
  50. data/app/views/flight_control/jobs/_job.html.erb +13 -0
  51. data/app/views/flight_control/jobs/_jobs_page.html.erb +13 -0
  52. data/app/views/flight_control/jobs/_raw_data.html.erb +4 -0
  53. data/app/views/flight_control/jobs/_title.html.erb +19 -0
  54. data/app/views/flight_control/jobs/_toolbar.html.erb +18 -0
  55. data/app/views/flight_control/jobs/blocked/_actions.html.erb +3 -0
  56. data/app/views/flight_control/jobs/blocked/_job.html.erb +8 -0
  57. data/app/views/flight_control/jobs/failed/_actions.html.erb +5 -0
  58. data/app/views/flight_control/jobs/failed/_backtrace_toggle.html.erb +14 -0
  59. data/app/views/flight_control/jobs/failed/_job.html.erb +8 -0
  60. data/app/views/flight_control/jobs/finished/_job.html.erb +3 -0
  61. data/app/views/flight_control/jobs/in_progress/_job.html.erb +9 -0
  62. data/app/views/flight_control/jobs/index.html.erb +19 -0
  63. data/app/views/flight_control/jobs/scheduled/_actions.html.erb +5 -0
  64. data/app/views/flight_control/jobs/scheduled/_job.html.erb +13 -0
  65. data/app/views/flight_control/jobs/show.html.erb +6 -0
  66. data/app/views/flight_control/queues/_actions.html.erb +7 -0
  67. data/app/views/flight_control/queues/_job.html.erb +15 -0
  68. data/app/views/flight_control/queues/_queue.html.erb +16 -0
  69. data/app/views/flight_control/queues/_queue_title.html.erb +17 -0
  70. data/app/views/flight_control/queues/index.html.erb +21 -0
  71. data/app/views/flight_control/queues/show.html.erb +25 -0
  72. data/app/views/flight_control/recurring_tasks/_actions.html.erb +5 -0
  73. data/app/views/flight_control/recurring_tasks/_general_information.html.erb +37 -0
  74. data/app/views/flight_control/recurring_tasks/_recurring_task.html.erb +22 -0
  75. data/app/views/flight_control/recurring_tasks/_title.html.erb +10 -0
  76. data/app/views/flight_control/recurring_tasks/index.html.erb +18 -0
  77. data/app/views/flight_control/recurring_tasks/show.html.erb +14 -0
  78. data/app/views/flight_control/shared/_job.html.erb +27 -0
  79. data/app/views/flight_control/shared/_jobs.html.erb +14 -0
  80. data/app/views/flight_control/shared/_pagination_toolbar.html.erb +7 -0
  81. data/app/views/flight_control/workers/_configuration.html.erb +6 -0
  82. data/app/views/flight_control/workers/_raw_data.html.erb +6 -0
  83. data/app/views/flight_control/workers/_title.html.erb +11 -0
  84. data/app/views/flight_control/workers/_worker.html.erb +21 -0
  85. data/app/views/flight_control/workers/_workers_page.html.erb +15 -0
  86. data/app/views/flight_control/workers/index.html.erb +6 -0
  87. data/app/views/flight_control/workers/show.html.erb +14 -0
  88. data/app/views/layouts/flight_control/_application_selection.html.erb +14 -0
  89. data/app/views/layouts/flight_control/_flash.html.erb +9 -0
  90. data/app/views/layouts/flight_control/_navigation.html.erb +9 -0
  91. data/app/views/layouts/flight_control/application.html.erb +28 -0
  92. data/app/views/layouts/flight_control/application_selection/_applications.html.erb +13 -0
  93. data/app/views/layouts/flight_control/application_selection/_servers.html.erb +15 -0
  94. data/config/importmap.rb +5 -0
  95. data/config/routes.rb +34 -0
  96. data/lib/active_job/job_proxy.rb +2 -1
  97. data/lib/active_job/jobs_relation.rb +17 -5
  98. data/lib/active_job/queue_adapters/solid_queue_ext.rb +43 -5
  99. data/lib/flight_control/engine.rb +7 -1
  100. data/lib/flight_control/host_route_helpers.rb +34 -0
  101. data/lib/flight_control/version.rb +1 -1
  102. data/lib/flight_control.rb +1 -0
  103. metadata +94 -1
@@ -0,0 +1,105 @@
1
+ module FlightControl::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 job_duration(job)
27
+ return unless (seconds = job.duration)
28
+ tag.span formatted_duration(seconds), title: "#{seconds.round(3)} seconds"
29
+ end
30
+
31
+ def attribute_names_for_job_status(status)
32
+ case status.to_s
33
+ when "failed" then ["Error", "Duration", ""]
34
+ when "blocked" then ["Queue", "Blocked by", ""]
35
+ when "finished" then ["Queue", "Duration", "Finished"]
36
+ when "scheduled" then ["Queue", "Scheduled", ""]
37
+ when "in_progress" then ["Queue", "Run by", "Running for"]
38
+ else []
39
+ end
40
+ end
41
+
42
+ def job_delayed?(job)
43
+ job.scheduled_at.before?(FlightControl.scheduled_job_delay_threshold.ago)
44
+ end
45
+
46
+ def blocked_job_expiration(job)
47
+ "Expires #{bidirectional_time_distance_in_words_with_title(job.blocked_until)}".html_safe if job.blocked_until
48
+ end
49
+
50
+ private
51
+
52
+ DURATION_UNIT_ABBREVIATIONS = {years: "y", months: "mo", weeks: "w", days: "d", hours: "h", minutes: "m", seconds: "s"}
53
+
54
+ def formatted_duration(seconds)
55
+ if seconds < 1
56
+ "#{(seconds * 1000).round} ms"
57
+ elsif seconds < 60
58
+ "#{seconds.round(1)} s"
59
+ else
60
+ ActiveSupport::Duration.build(seconds.round).parts
61
+ .map { |unit, value| "#{value}#{DURATION_UNIT_ABBREVIATIONS[unit]}" }.join(" ")
62
+ end
63
+ end
64
+
65
+ def renderable_job_arguments_for(job)
66
+ job.serialized_arguments.collect do |argument|
67
+ as_renderable_argument(argument)
68
+ end
69
+ end
70
+
71
+ def as_renderable_argument(argument)
72
+ case argument
73
+ when Hash
74
+ as_renderable_hash(argument)
75
+ when Array
76
+ as_renderable_array(argument)
77
+ else
78
+ ActiveJob::Arguments.deserialize([argument]).first
79
+ end
80
+ rescue ActiveJob::DeserializationError
81
+ argument.to_s
82
+ end
83
+
84
+ def as_renderable_hash(argument)
85
+ if argument["_aj_globalid"]
86
+ # don't deserialize as the class might not exist in the host app running the engine
87
+ argument["_aj_globalid"]
88
+ elsif argument["_aj_serialized"] == "ActiveJob::Serializers::ModuleSerializer"
89
+ argument["value"]
90
+ elsif argument["_aj_serialized"]
91
+ ActiveJob::Arguments.deserialize([argument]).first
92
+ else
93
+ FlightControl.job_arguments_filter.apply_to(argument)
94
+ .without("_aj_symbol_keys", "_aj_ruby2_keywords")
95
+ .transform_values { |v| as_renderable_argument(v) }
96
+ .map { |k, v| "#{k}: #{v}" }
97
+ .join(", ")
98
+ .then { |s| "{#{s}}" }
99
+ end
100
+ end
101
+
102
+ def as_renderable_array(argument)
103
+ "[#{argument.collect { |part| as_renderable_argument(part) }.join(", ")}]"
104
+ end
105
+ end
@@ -0,0 +1,61 @@
1
+ module FlightControl::NavigationHelper
2
+ attr_reader :page_title, :current_section
3
+
4
+ def back_to_main_app_path
5
+ FlightControl.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
+ sections[:job_launches] = ["Run job", new_application_job_launch_path(@application)]
17
+ end
18
+ end
19
+
20
+ def navigation_section_for_status(status)
21
+ if status.nil? || status == :pending
22
+ :queues
23
+ else
24
+ :"#{status}_jobs"
25
+ end
26
+ end
27
+
28
+ def navigation(title: nil, section: nil)
29
+ @page_title = title
30
+ @current_section = section
31
+ end
32
+
33
+ def selected_application?(application)
34
+ FlightControl::Current.application.name == application.name
35
+ end
36
+
37
+ def selectable_applications
38
+ FlightControl.applications.reject { |app| selected_application?(app) }
39
+ end
40
+
41
+ def selected_server?(server)
42
+ FlightControl::Current.server.name == server.name
43
+ end
44
+
45
+ def jobs_count_with_status(status)
46
+ count = ActiveJob.jobs.with_status(status).count
47
+ if count.infinite?
48
+ "..."
49
+ else
50
+ number_to_human(count,
51
+ format: "%n%u",
52
+ units: {
53
+ thousand: "K",
54
+ million: "M",
55
+ billion: "B",
56
+ trillion: "T",
57
+ quadrillion: "Q"
58
+ })
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,3 @@
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"
@@ -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,8 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ submit(event) {
5
+ const form = event.target.form || event.target.closest("form")
6
+ if (form) form.requestSubmit()
7
+ }
8
+ }
@@ -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,27 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Replaces a server-rendered UTC timestamp with "<browser-local time> · <UTC time>".
4
+ // Leaves the UTC-only fallback in place when the browser's timezone is UTC.
5
+ export default class extends Controller {
6
+ static values = { datetime: String }
7
+
8
+ connect() {
9
+ const date = new Date(this.datetimeValue)
10
+ if (isNaN(date)) return
11
+
12
+ const local = this.format(date)
13
+ const utc = this.format(date, "UTC")
14
+ this.element.textContent = local === utc ? utc : `${local} · ${utc}`
15
+ }
16
+
17
+ format(date, timeZone = undefined) {
18
+ // en-CA yields "2026-07-10, 12:32:15 UTC"
19
+ return new Intl.DateTimeFormat("en-CA", {
20
+ timeZone,
21
+ year: "numeric", month: "2-digit", day: "2-digit",
22
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
23
+ hourCycle: "h23",
24
+ timeZoneName: "short"
25
+ }).format(date).replace(",", "")
26
+ }
27
+ }
@@ -0,0 +1,5 @@
1
+ module FlightControl
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ class FlightControl::Current < ActiveSupport::CurrentAttributes
2
+ attribute :application, :server
3
+ end
@@ -0,0 +1,75 @@
1
+ class FlightControl::JobLaunch
2
+ include ActiveModel::Model
3
+ include ActiveModel::Attributes
4
+
5
+ attribute :job_class_name, :string
6
+ attribute :arguments_json, :string, default: "[]"
7
+ attribute :queue_name, :string
8
+
9
+ validates :job_class_name, presence: true
10
+ validate :validate_job_class
11
+ validate :validate_arguments
12
+
13
+ # Job classes defined in the host application. Not memoized on purpose:
14
+ # class reloading in development would leave stale classes behind.
15
+ def self.job_classes
16
+ Rails.application.eager_load! unless Rails.application.config.eager_load
17
+ ActiveJob::Base.descendants.select { |job_class| job_class.name.present? && implements_perform?(job_class) }.sort_by(&:name)
18
+ end
19
+
20
+ # ActiveJob::Execution defines a perform placeholder that raises NotImplementedError,
21
+ # so abstract base classes like ApplicationJob still own that placeholder.
22
+ def self.implements_perform?(job_class)
23
+ job_class.instance_method(:perform).owner != ActiveJob::Execution
24
+ end
25
+
26
+ def enqueue
27
+ return nil unless valid?
28
+
29
+ job = configured_job_class.perform_later(*positional_arguments, **keyword_arguments)
30
+ if job&.successfully_enqueued?
31
+ job
32
+ else
33
+ errors.add(:base, "job could not be enqueued#{": #{job.enqueue_error.message}" if job&.enqueue_error}")
34
+ nil
35
+ end
36
+ rescue => error
37
+ errors.add(:base, "job could not be enqueued: #{error.message}")
38
+ nil
39
+ end
40
+
41
+ private
42
+
43
+ def job_class
44
+ # Only classes from the discovered list can be launched; never constantize user input.
45
+ self.class.job_classes.detect { |klass| klass.name == job_class_name }
46
+ end
47
+
48
+ def configured_job_class
49
+ queue_name.present? ? job_class.set(queue: queue_name) : job_class
50
+ end
51
+
52
+ def parsed_arguments
53
+ @parsed_arguments ||= JSON.parse(arguments_json.presence || "[]")
54
+ end
55
+
56
+ def positional_arguments
57
+ parsed_arguments.last.is_a?(Hash) ? parsed_arguments[0...-1] : parsed_arguments
58
+ end
59
+
60
+ def keyword_arguments
61
+ parsed_arguments.last.is_a?(Hash) ? parsed_arguments.last.transform_keys(&:to_sym) : {}
62
+ end
63
+
64
+ def validate_job_class
65
+ if job_class_name.present? && job_class.nil?
66
+ errors.add(:job_class_name, "is not a job class of this application")
67
+ end
68
+ end
69
+
70
+ def validate_arguments
71
+ errors.add(:arguments_json, "must be a JSON array") unless parsed_arguments.is_a?(Array)
72
+ rescue JSON::ParserError
73
+ errors.add(:arguments_json, "is not valid JSON")
74
+ end
75
+ end
@@ -0,0 +1,49 @@
1
+ class FlightControl::Page
2
+ DEFAULT_PAGE_SIZE = 10
3
+
4
+ attr_reader :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
+
46
+ def offset
47
+ (index - 1) * page_size
48
+ end
49
+ end
@@ -0,0 +1,26 @@
1
+ class FlightControl::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
+
25
+ attr_reader :queue_adapter
26
+ end
@@ -0,0 +1,18 @@
1
+ class FlightControl::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
+
17
+ attr_reader :queue_adapter
18
+ end
@@ -0,0 +1,53 @@
1
+ <% navigation(title: "Run job", section: :job_launches) %>
2
+
3
+ <%= form_with model: @job_launch, url: application_job_launches_path(@application) do |form| %>
4
+ <% if @job_launch.errors.any? %>
5
+ <article class="message is-danger">
6
+ <div class="message-body">
7
+ <ul>
8
+ <% @job_launch.errors.full_messages.each do |message| %>
9
+ <li><%= message %></li>
10
+ <% end %>
11
+ </ul>
12
+ </div>
13
+ </article>
14
+ <% end %>
15
+
16
+ <div class="field">
17
+ <%= form.label :job_class_name, "Job class", class: "label" %>
18
+ <div class="control">
19
+ <div class="select">
20
+ <%= form.select :job_class_name, @job_classes.map(&:name), include_blank: "Select a job class..." %>
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="field">
26
+ <%= form.label :arguments_json, "Arguments", class: "label" %>
27
+ <div class="control">
28
+ <%= form.text_area :arguments_json, class: "textarea is-family-monospace", rows: 4, placeholder: '[ 123, "abc", { "some_option": true } ]' %>
29
+ </div>
30
+ <p class="help">
31
+ A JSON array with the job's arguments. A trailing JSON object is passed as keyword arguments.
32
+ Leave <code>[]</code> to run the job without arguments.
33
+ </p>
34
+ </div>
35
+
36
+ <div class="field">
37
+ <%= form.label :queue_name, "Queue (optional)", class: "label" %>
38
+ <div class="control">
39
+ <%= form.text_field :queue_name, class: "input", list: "queue-names", placeholder: "Job class default", autocomplete: "off" %>
40
+ </div>
41
+ <datalist id="queue-names" class="is-hidden">
42
+ <% @queue_names.each do |queue_name| %>
43
+ <option value="<%= queue_name %>"></option>
44
+ <% end %>
45
+ </datalist>
46
+ </div>
47
+
48
+ <div class="field">
49
+ <div class="control">
50
+ <%= form.submit "Enqueue job", class: "button is-primary" %>
51
+ </div>
52
+ </div>
53
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <%# locals: (form:, attribute:, label:) %>
2
+
3
+ <div class="control">
4
+ <%= form.label :"#{attribute}_start", "#{label} from", class: "label" %>
5
+ <%= form.datetime_field :"#{attribute}_start", value: @job_filters[attribute]&.begin, class: "input is-rounded" %>
6
+ </div>
7
+
8
+ <div class="control">
9
+ <%= form.label :"#{attribute}_end", "#{label} to", class: "label" %>
10
+ <%= form.datetime_field :"#{attribute}_end", value: @job_filters[attribute]&.end, class: "input is-rounded" %>
11
+ </div>
@@ -0,0 +1,24 @@
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
+ <%= turbo_frame_tag "job-backtrace" do %>
18
+ <% if @server.backtrace_cleaner %>
19
+ <%= render "flight_control/jobs/failed/backtrace_toggle", application: @application, job: job %>
20
+ <% end %>
21
+
22
+ <pre class="is-family-monospace mb-4 backtrace-content"><%= failed_job_backtrace(job, @server) %></pre>
23
+ <% end %>
24
+ <% end %>
@@ -0,0 +1,49 @@
1
+ <div class="filter level-left">
2
+ <%= form_for :filter, url: application_jobs_path(FlightControl::Current.application, jobs_status), method: :get,
3
+ data: { controller: "form", action: "change->form#submit" } do |form| %>
4
+
5
+ <div class="field is-grouped is-grouped-multiline">
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
+ <%= render "flight_control/jobs/date_range_filter", form: form, attribute: :finished_at, label: "Finished" %>
22
+ <% else %>
23
+ <%= render "flight_control/jobs/date_range_filter", form: form, attribute: :enqueued_at, label: "Enqueued" %>
24
+ <% end %>
25
+
26
+ <% if jobs_status == "scheduled" %>
27
+ <%= render "flight_control/jobs/date_range_filter", form: form, attribute: :scheduled_at, label: "Scheduled" %>
28
+ <% end %>
29
+
30
+ <%= hidden_field_tag :server_id, FlightControl::Current.server.id %>
31
+
32
+ <datalist id="job-classes" class="is-hidden">
33
+ <% job_class_names.each do |job_class_name| %>
34
+ <option value="<%= job_class_name %>"></option>
35
+ <% end %>
36
+ </datalist>
37
+
38
+ <datalist id="queue-names" class="is-hidden">
39
+ <% queue_names.each do |queue_name| %>
40
+ <option value="<%= queue_name %>"></option>
41
+ <% end %>
42
+ </datalist>
43
+
44
+ <div class="control is-align-self-flex-end">
45
+ <%= link_to "Clear", application_jobs_path(FlightControl::Current.application, jobs_status), class: "button" %>
46
+ </div>
47
+ </div>
48
+ <% end %>
49
+ </div>
@@ -0,0 +1,77 @@
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
+ <%= timestamp_with_relative_time(job.enqueued_at.to_datetime) %>
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
+ <%= timestamp_with_relative_time(job.finished_at) %>
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
+ </tbody>
77
+ </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"><%= truncate(job_arguments(job), length: 300) %></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 "flight_control/jobs/#{jobs_status}/job", job: job %>
13
+ </tr>
@@ -0,0 +1,13 @@
1
+ <table class="jobs <%= jobs_status %> table queues is-hoverable is-fullwidth">
2
+ <thead>
3
+ <tr>
4
+ <th class="job-header">Job</th>
5
+ <% attribute_names_for_job_status(jobs_status).each do |attribute| %>
6
+ <th><%= attribute %></th>
7
+ <% end %>
8
+ </tr>
9
+ </thead>
10
+ <tbody>
11
+ <%= render partial: "flight_control/jobs/job", collection: jobs_page.records %>
12
+ </tbody>
13
+ </table>
@@ -0,0 +1,4 @@
1
+ <h2 class="subtitle">Raw data</h2>
2
+ <pre>
3
+ <%= JSON.pretty_generate(job.filtered_raw_data.without("backtrace")) %>
4
+ </pre>