flightdeck 0.5.2 → 0.5.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/flightdeck/{flightdeck-00403457c5d9.css → flightdeck-3967b9e6c506.css} +1 -1
  3. data/app/assets/flightdeck/{flightdeck-747ab6d1a6fa.js → flightdeck-3aac23f80afe.js} +2 -2
  4. data/app/assets/flightdeck/manifest.json +7 -7
  5. data/app/controllers/concerns/flightdeck/toasts.rb +23 -13
  6. data/app/controllers/flightdeck/jobs/actions_controller.rb +41 -42
  7. data/app/controllers/flightdeck/jobs/discards_controller.rb +0 -1
  8. data/app/controllers/flightdeck/jobs/retries_controller.rb +0 -1
  9. data/app/controllers/flightdeck/processes_controller.rb +8 -3
  10. data/app/controllers/flightdeck/queues_controller.rb +8 -3
  11. data/app/controllers/flightdeck/recurring_tasks_controller.rb +7 -2
  12. data/app/helpers/flightdeck/application_helper.rb +17 -0
  13. data/app/helpers/flightdeck/jobs_helper.rb +1 -20
  14. data/app/models/flightdeck/duration.rb +23 -0
  15. data/app/models/flightdeck/job_detail.rb +1 -8
  16. data/app/models/flightdeck/job_row.rb +10 -0
  17. data/app/models/flightdeck/jobs_query.rb +8 -16
  18. data/app/models/flightdeck/metrics/line_chart.rb +3 -7
  19. data/app/models/flightdeck/overview.rb +12 -19
  20. data/app/models/flightdeck/process_registry.rb +0 -6
  21. data/app/models/flightdeck/queue_stats.rb +0 -2
  22. data/app/models/flightdeck/recurring_catalog.rb +0 -2
  23. data/app/views/flightdeck/jobs/_list.html.erb +9 -4
  24. data/app/views/flightdeck/jobs/index.html.erb +2 -5
  25. data/app/views/flightdeck/overview/_tiles.html.erb +1 -1
  26. data/app/views/flightdeck/overview/index.html.erb +12 -37
  27. data/app/views/flightdeck/processes/index.html.erb +2 -6
  28. data/app/views/flightdeck/queues/index.html.erb +2 -6
  29. data/app/views/flightdeck/recurring_tasks/index.html.erb +2 -6
  30. data/app/views/flightdeck/shared/refresh.turbo_stream.erb +14 -0
  31. data/lib/flightdeck/assets.rb +0 -6
  32. data/lib/flightdeck/version.rb +1 -1
  33. metadata +5 -7
  34. data/app/views/flightdeck/jobs/actions/create.turbo_stream.erb +0 -18
  35. data/app/views/flightdeck/processes/prune.turbo_stream.erb +0 -15
  36. data/app/views/flightdeck/queues/update.turbo_stream.erb +0 -15
  37. data/app/views/flightdeck/recurring_tasks/run.turbo_stream.erb +0 -15
@@ -15,7 +15,6 @@ module Flightdeck
15
15
  execution.retry
16
16
  end
17
17
 
18
- def verb = "retry"
19
18
  def past_tense = "retried"
20
19
  def blocked_reason = "failed"
21
20
  end
@@ -19,7 +19,7 @@ module Flightdeck
19
19
  unless node.prunable?
20
20
  toast "#{node.kind} #{node.name} is still sending heartbeats — only unresponsive " \
21
21
  "processes can be pruned.", level: :error
22
- return respond_with_toast("flightdeck/processes/prune", fallback: processes_path)
22
+ return respond_with_toast(processes_frame, fallback: processes_path)
23
23
  end
24
24
 
25
25
  claimed = node.claimed_count
@@ -27,7 +27,7 @@ module Flightdeck
27
27
  load_registry
28
28
 
29
29
  toast "Pruned #{node.kind} #{node.name}#{released(claimed)}."
30
- respond_with_toast "flightdeck/processes/prune", fallback: processes_path
30
+ respond_with_toast processes_frame, fallback: processes_path
31
31
  end
32
32
 
33
33
  private
@@ -35,6 +35,11 @@ module Flightdeck
35
35
  @registry = ProcessRegistry.new
36
36
  end
37
37
 
38
+ def processes_frame
39
+ { id: "fd-processes", url: processes_path,
40
+ partial: "flightdeck/processes/fleet", locals: { registry: @registry } }
41
+ end
42
+
38
43
  def released(claimed)
39
44
  return "" if claimed.zero?
40
45
 
@@ -43,7 +48,7 @@ module Flightdeck
43
48
 
44
49
  def missing
45
50
  toast "That process is no longer registered.", level: :error
46
- respond_with_toast "flightdeck/processes/prune", fallback: processes_path
51
+ respond_with_toast processes_frame, fallback: processes_path
47
52
  end
48
53
  end
49
54
  end
@@ -16,14 +16,14 @@ module Flightdeck
16
16
  SolidQueue::Queue.new(@queue_name).pause
17
17
  reload_stats
18
18
  toast "Paused #{@queue_name}. Workers will stop picking up its jobs."
19
- respond_with_toast "flightdeck/queues/update", fallback: queues_path
19
+ respond_with_toast queues_frame, fallback: queues_path
20
20
  end
21
21
 
22
22
  def resume
23
23
  SolidQueue::Queue.new(@queue_name).resume
24
24
  reload_stats
25
25
  toast "Resumed #{@queue_name}."
26
- respond_with_toast "flightdeck/queues/update", fallback: queues_path
26
+ respond_with_toast queues_frame, fallback: queues_path
27
27
  end
28
28
 
29
29
  private
@@ -36,6 +36,11 @@ module Flightdeck
36
36
  Flightdeck::Cache.bypass { load_stats }
37
37
  end
38
38
 
39
+ def queues_frame
40
+ { id: "fd-queues", url: queues_path,
41
+ partial: "flightdeck/queues/cards", locals: { stats: @stats, sparklines: @sparklines } }
42
+ end
43
+
39
44
  # Only queues Flightdeck is actually showing can be paused. That keeps an
40
45
  # arbitrary parameter from creating pause rows for queues that do not
41
46
  # exist.
@@ -45,7 +50,7 @@ module Flightdeck
45
50
  return if @stats.find(@queue_name)
46
51
 
47
52
  toast "Unknown queue #{@queue_name.presence || "(blank)"}.", level: :error
48
- respond_with_toast "flightdeck/queues/update", fallback: queues_path
53
+ respond_with_toast queues_frame, fallback: queues_path
49
54
  end
50
55
  end
51
56
  end
@@ -26,7 +26,7 @@ module Flightdeck
26
26
  "class refused to enqueue.", level: :error
27
27
  end
28
28
 
29
- respond_with_toast "flightdeck/recurring_tasks/run", fallback: recurring_tasks_path
29
+ respond_with_toast recurring_frame, fallback: recurring_tasks_path
30
30
  end
31
31
 
32
32
  private
@@ -34,13 +34,18 @@ module Flightdeck
34
34
  @catalog = RecurringCatalog.new
35
35
  end
36
36
 
37
+ def recurring_frame
38
+ { id: "fd-recurring", url: recurring_tasks_path,
39
+ partial: "flightdeck/recurring_tasks/table", locals: { catalog: @catalog } }
40
+ end
41
+
37
42
  def job_reference
38
43
  @enqueued_job_id ? " as job ##{@enqueued_job_id}" : ""
39
44
  end
40
45
 
41
46
  def missing
42
47
  toast "That recurring task no longer exists.", level: :error
43
- respond_with_toast "flightdeck/recurring_tasks/run", fallback: recurring_tasks_path
48
+ respond_with_toast recurring_frame, fallback: recurring_tasks_path
44
49
  end
45
50
  end
46
51
  end
@@ -2,6 +2,23 @@
2
2
 
3
3
  module Flightdeck
4
4
  module ApplicationHelper
5
+ # The polling <turbo-frame> every panel lives in: the refresh Stimulus
6
+ # controller, its interval, and the URL the frame re-requests. One owner for
7
+ # the markup, so the index views and the turbo-stream replacements can never
8
+ # drift apart.
9
+ #
10
+ # `target:` defaults to "_top" because most frames are lists whose links
11
+ # leave the frame; the jobs list opts out (`target: nil`) so its pager can
12
+ # navigate in place.
13
+ def fd_refresh_frame(id, url:, interval: fd_poll_ms, target: "_top", **options, &block)
14
+ tag.turbo_frame(
15
+ id: id,
16
+ target: target,
17
+ data: { controller: "refresh", refresh_interval_value: interval, refresh_url_value: url },
18
+ **options, &block
19
+ )
20
+ end
21
+
5
22
  # Path to a built asset, resolved through the committed manifest. Returns
6
23
  # nil when assets have not been built yet (fresh checkout / dev), so the
7
24
  # layout can simply omit the tag instead of blowing up.
@@ -54,16 +54,7 @@ module Flightdeck
54
54
  end
55
55
 
56
56
  def fd_duration(seconds)
57
- return "—" if seconds.nil?
58
-
59
- seconds = seconds.to_f.abs
60
- case seconds
61
- when 0...1 then "#{(seconds * 1000).round}ms"
62
- when 1...60 then "#{seconds.round(1)}s"
63
- when 60...3600 then "#{(seconds / 60).round}m"
64
- when 3600...86_400 then "#{(seconds / 3600).round(1)}h"
65
- else "#{(seconds / 86_400).round(1)}d"
66
- end
57
+ seconds.nil? ? "—" : Flightdeck::Duration.humanize(seconds)
67
58
  end
68
59
 
69
60
  def fd_ago(time)
@@ -102,10 +93,6 @@ module Flightdeck
102
93
  Flightdeck::ArgumentsPreview.format(preview, length: length)
103
94
  end
104
95
 
105
- def fd_jobs_url(overrides = {})
106
- jobs_path(list_filters.merge(state: params[:state].presence).compact.merge(overrides))
107
- end
108
-
109
96
  # The frame refreshes itself by re-requesting the URL it is showing, so
110
97
  # filters, state tab and cursor all survive a poll.
111
98
  def fd_current_list_url
@@ -123,12 +110,6 @@ module Flightdeck
123
110
  text.to_s
124
111
  end
125
112
 
126
- # URL the refreshed frame should poll after an action, rebuilt from the
127
- # filters the action carried rather than from the POST's own path.
128
- def fd_refreshed_list_url
129
- jobs_path(list_filters.merge(state: @refreshed_state == :all ? nil : @refreshed_state).compact)
130
- end
131
-
132
113
  def fd_toast_level_class(level)
133
114
  level.to_sym == :error ? "fd-toast error" : "fd-toast"
134
115
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flightdeck
4
+ # The one way a number of seconds is written anywhere in Flightdeck —
5
+ # list columns, overview tiles and chart axes all speak through this, so
6
+ # "how long" can never be spelled three subtly different ways.
7
+ module Duration
8
+ module_function
9
+
10
+ def humanize(seconds)
11
+ value = seconds.to_f.abs
12
+ case value
13
+ when 0...1 then "#{(value * 1000).round}ms"
14
+ when 1...60 then "#{trim(value.round(1))}s"
15
+ when 60...3600 then "#{(value / 60).round}m"
16
+ when 3600...86_400 then "#{trim((value / 3600).round(1))}h"
17
+ else "#{trim((value / 86_400).round(1))}d"
18
+ end
19
+ end
20
+
21
+ def trim(number) = number.to_s.sub(/\.0\z/, "")
22
+ end
23
+ end
@@ -27,7 +27,7 @@ module Flightdeck
27
27
 
28
28
  def initialize(job:, annotation: nil)
29
29
  @job = job
30
- @state = resolve_state(annotation)
30
+ @state = JobRow.state_for(job, annotation)
31
31
  @execution = load_execution(annotation)
32
32
  @process = load_process
33
33
  end
@@ -140,13 +140,6 @@ module Flightdeck
140
140
  end
141
141
 
142
142
  private
143
- def resolve_state(annotation)
144
- return :finished if job.finished_at.present?
145
- return annotation[:state] if annotation
146
-
147
- :unknown
148
- end
149
-
150
143
  def load_execution(annotation)
151
144
  return nil unless annotation
152
145
 
@@ -21,6 +21,16 @@ module Flightdeck
21
21
  name && name.constantize
22
22
  end
23
23
 
24
+ # How a job's state is derived, in one place: `finished_at` wins, then
25
+ # whichever execution table `annotate` found the job in, then :unknown
26
+ # for the transition window where a job is between tables.
27
+ def state_for(job, annotation)
28
+ return :finished if job.finished_at.present?
29
+ return annotation[:state] if annotation
30
+
31
+ :unknown
32
+ end
33
+
24
34
  # Resolves the state of an already-loaded page of jobs with one
25
35
  # `WHERE job_id IN (page_ids)` lookup per executions table. Returns
26
36
  # { job_id => { state:, execution_id:, process_id: } }.
@@ -90,7 +90,9 @@ module Flightdeck
90
90
  end
91
91
 
92
92
  private
93
- JOB_LIST_COLUMNS = -> { SolidQueue::Job.column_names - [ "arguments" ] }
93
+ # The columns list queries must never drag along: `arguments` on jobs and
94
+ # `error` on failed_executions can each run to kilobytes per row.
95
+ PAYLOAD_COLUMNS = %w[arguments error].freeze
94
96
 
95
97
  # Only the filters change what a count means — the cursor and page size do
96
98
  # not, so they are deliberately absent from the key.
@@ -124,11 +126,10 @@ module Flightdeck
124
126
  relation.select(driving_columns).to_a
125
127
  end
126
128
 
127
- # Never `SELECT *`: it would drag `arguments` (jobs) or `error`
128
- # (failed_executions) through every list query.
129
+ # Never `SELECT *`: it would drag a payload column through every list
130
+ # query.
129
131
  def driving_columns
130
- names = model.column_names - [ "arguments", "error" ]
131
- names.map { |name| model.arel_table[name] }
132
+ (model.column_names - PAYLOAD_COLUMNS).map { |name| model.arel_table[name] }
132
133
  end
133
134
 
134
135
  def rows_from_executions(executions)
@@ -159,25 +160,16 @@ module Flightdeck
159
160
  annotations = state == :finished ? {} : JobRow.annotate(job_ids)
160
161
 
161
162
  jobs.map do |job|
162
- annotation = annotations[job.id]
163
-
164
163
  JobRow.new(
165
164
  job: job,
166
- state: state_for(job, annotation),
165
+ state: JobRow.state_for(job, annotations[job.id]),
167
166
  args_preview: previews[job.id]
168
167
  )
169
168
  end
170
169
  end
171
170
 
172
- def state_for(job, annotation)
173
- return :finished if job.finished_at.present?
174
- return annotation[:state] if annotation
175
-
176
- :unknown
177
- end
178
-
179
171
  def jobs_by_id(job_ids)
180
- SolidQueue::Job.where(id: job_ids).select(JOB_LIST_COLUMNS.call).index_by(&:id)
172
+ SolidQueue::Job.where(id: job_ids).select(SolidQueue::Job.column_names - PAYLOAD_COLUMNS).index_by(&:id)
181
173
  end
182
174
 
183
175
  def processes_by_id(process_ids)
@@ -49,14 +49,10 @@ module Flightdeck
49
49
  private
50
50
  def raw_max = points.filter_map(&:seconds).max.to_f
51
51
 
52
+ # The zero gridline reads "0s" rather than "0ms": it is an axis origin,
53
+ # not a measurement.
52
54
  def format_value(value)
53
- case value
54
- when 0 then "0s"
55
- when 0...1 then "#{(value * 1000).round}ms"
56
- when 1...60 then "#{value.round(1).to_s.sub(/\.0\z/, "")}s"
57
- when 60...3600 then "#{(value / 60).round}m"
58
- else "#{(value / 3600).round(1)}h"
59
- end
55
+ value.zero? ? "0s" : Flightdeck::Duration.humanize(value)
60
56
  end
61
57
 
62
58
  def point_title(point)
@@ -8,6 +8,8 @@ module Flightdeck
8
8
  RECENT_FAILURES = 5
9
9
  TOP_QUEUES = 6
10
10
 
11
+ # `value` is always display-ready text — a delimited count, a duration, or
12
+ # "—" — so the view prints it without asking what kind of number it was.
11
13
  # trend is :up / :down / :flat and says which way the number moved, while
12
14
  # `good` says whether that movement is a good thing — more throughput is
13
15
  # good, more failures are not.
@@ -143,7 +145,7 @@ module Flightdeck
143
145
 
144
146
  Tile.new(
145
147
  label: "Processed · 24h",
146
- value: processed_24h,
148
+ value: number(processed_24h),
147
149
  detail: delta.nil? ? "no prior window to compare" : "#{delta.abs}% vs prior 24h",
148
150
  trend: trend_for(delta),
149
151
  good: delta.nil? || delta >= 0
@@ -155,7 +157,7 @@ module Flightdeck
155
157
 
156
158
  Tile.new(
157
159
  label: "Failed · 24h",
158
- value: failed_24h,
160
+ value: number(failed_24h),
159
161
  detail: rate.nil? ? "nothing finished yet" : "#{rate}% failure rate",
160
162
  trend: failed_24h.positive? ? :up : :flat,
161
163
  good: failed_24h.zero?
@@ -163,24 +165,24 @@ module Flightdeck
163
165
  end
164
166
 
165
167
  def ready_tile
166
- Tile.new(label: "Ready now", value: ready_now, detail: "waiting to be claimed", trend: :flat, good: true)
168
+ Tile.new(label: "Ready now", value: number(ready_now), detail: "waiting to be claimed", trend: :flat, good: true)
167
169
  end
168
170
 
169
171
  def scheduled_tile
170
172
  detail =
171
173
  if next_scheduled_at.nil? then "nothing scheduled"
172
174
  elsif next_scheduled_at <= now then "due now"
173
- else "next due in #{humanize_duration(next_scheduled_at - now)}"
175
+ else "next due in #{Duration.humanize(next_scheduled_at - now)}"
174
176
  end
175
177
 
176
- Tile.new(label: "Scheduled", value: scheduled, detail: detail, trend: :flat, good: true)
178
+ Tile.new(label: "Scheduled", value: number(scheduled), detail: detail, trend: :flat, good: true)
177
179
  end
178
180
 
179
181
  def in_progress_tile
180
182
  Tile.new(
181
183
  label: "In progress",
182
- value: in_progress,
183
- unit: slots ? "/ #{ActiveSupport::NumberHelper.number_to_delimited(slots)} slots" : nil,
184
+ value: number(in_progress),
185
+ unit: slots ? "/ #{number(slots)} slots" : nil,
184
186
  detail: utilization ? "#{utilization}% utilization" : "no worker capacity reported",
185
187
  trend: :flat,
186
188
  good: true
@@ -190,28 +192,19 @@ module Flightdeck
190
192
  def oldest_ready_tile
191
193
  Tile.new(
192
194
  label: "Oldest ready",
193
- value: oldest_ready_age ? humanize_duration(oldest_ready_age) : "—",
195
+ value: oldest_ready_age ? Duration.humanize(oldest_ready_age) : "—",
194
196
  detail: oldest_ready_age ? "longest a job has waited" : "nothing waiting",
195
197
  trend: :flat,
196
198
  good: true
197
199
  )
198
200
  end
199
201
 
202
+ def number(value) = ActiveSupport::NumberHelper.number_to_delimited(value)
203
+
200
204
  def trend_for(delta)
201
205
  return :flat if delta.nil? || delta.abs < 0.05
202
206
 
203
207
  delta.positive? ? :up : :down
204
208
  end
205
-
206
- def humanize_duration(seconds)
207
- seconds = seconds.to_f.abs
208
- case seconds
209
- when 0...1 then "#{(seconds * 1000).round}ms"
210
- when 1...60 then "#{seconds.round(1).to_s.sub(/\.0\z/, "")}s"
211
- when 60...3600 then "#{(seconds / 60).round}m"
212
- when 3600...86_400 then "#{(seconds / 3600).round(1)}h"
213
- else "#{(seconds / 86_400).round(1)}d"
214
- end
215
- end
216
209
  end
217
210
  end
@@ -63,8 +63,6 @@ module Flightdeck
63
63
  end
64
64
  end
65
65
 
66
- def self.all = new
67
-
68
66
  def nodes
69
67
  @nodes ||= build_nodes
70
68
  end
@@ -82,10 +80,6 @@ module Flightdeck
82
80
  end
83
81
  end
84
82
 
85
- def flattened
86
- tree.flat_map { |root| [ root, *root.children ] }
87
- end
88
-
89
83
  def dead = nodes.select(&:dead?)
90
84
  def any_dead? = dead.any?
91
85
  def dead_claimed_count = dead.sum(&:claimed_count)
@@ -31,8 +31,6 @@ module Flightdeck
31
31
  def idle? = depth.zero? && completed_in_window.zero?
32
32
  end
33
33
 
34
- def self.all = new.rows
35
-
36
34
  def rows
37
35
  @rows ||= names.sort.map do |name|
38
36
  Row.new(
@@ -41,8 +41,6 @@ module Flightdeck
41
41
  def ever_run? = last_run_at.present?
42
42
  end
43
43
 
44
- def self.all = new.rows
45
-
46
44
  def rows
47
45
  @rows ||= tasks.map do |task|
48
46
  run = last_runs[task.key]
@@ -9,10 +9,15 @@
9
9
  formaction="<%= bulk_discard_jobs_path(list_filters.merge(state: :failed)) %>"
10
10
  data-turbo-confirm="Permanently discard the selected jobs? This cannot be undone.">Discard selected</button>
11
11
  <span class="spacer"></span>
12
- <button type="submit" name="scope" value="all" class="fd-btn sm"
13
- data-turbo-confirm="Retry all failed jobs matching the current filter?">
14
- or apply to all <%= fd_count(query.count, capped: query.count_capped?) %> matching
15
- </button>
12
+ <%# Each "all matching" button names its own action: a single neutral
13
+ "apply to all" button inherited the form's action and silently
14
+ retried when the user meant to discard. %>
15
+ <span class="fd-bulkbar-all">or all <%= fd_count(query.count, capped: query.count_capped?) %> matching:</span>
16
+ <button type="submit" name="scope" value="all" class="fd-btn sm primary"
17
+ data-turbo-confirm="Retry all failed jobs matching the current filter?">Retry all</button>
18
+ <button type="submit" name="scope" value="all" class="fd-btn sm danger"
19
+ formaction="<%= bulk_discard_jobs_path(list_filters.merge(state: :failed)) %>"
20
+ data-turbo-confirm="Permanently discard all failed jobs matching the current filter? This cannot be undone.">Discard all</button>
16
21
  </div>
17
22
 
18
23
  <div class="fd-card">
@@ -5,9 +5,6 @@
5
5
 
6
6
  <%# The frame re-requests the URL it is showing, so state, filters and cursor
7
7
  all survive a poll. %>
8
- <turbo-frame id="fd-jobs"
9
- data-controller="refresh"
10
- data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
11
- data-refresh-url-value="<%= fd_current_list_url %>">
8
+ <%= fd_refresh_frame "fd-jobs", url: fd_current_list_url, target: nil do %>
12
9
  <%= render "flightdeck/jobs/list", query: @query, rows: @rows, groups: @groups %>
13
- </turbo-frame>
10
+ <% end %>
@@ -3,7 +3,7 @@
3
3
  <div class="fd-card fd-tile">
4
4
  <div class="lbl"><%= tile.label %></div>
5
5
  <div class="val">
6
- <%= tile.value.is_a?(Numeric) ? number_with_delimiter(tile.value) : tile.value %><%
6
+ <%= tile.value %><%
7
7
  %><% if tile.unit %><small><%= tile.unit %></small><% end %>
8
8
  </div>
9
9
  <div class="delta <%= fd_delta_class(tile) %>">
@@ -1,54 +1,29 @@
1
1
  <% content_for :page_title, "Overview" %>
2
2
 
3
- <turbo-frame id="fd-tiles"
4
- target="_top"
5
- data-controller="refresh"
6
- data-refresh-interval-value="<%= fd_poll_ms %>"
7
- data-refresh-url-value="<%= root_path(range: @window.key) %>">
3
+ <%= fd_refresh_frame "fd-tiles", url: root_path(range: @window.key) do %>
8
4
  <%= render "flightdeck/overview/tiles", overview: @overview %>
9
- </turbo-frame>
5
+ <% end %>
10
6
 
11
7
  <div class="fd-grid-2">
12
- <turbo-frame id="fd-throughput"
13
- target="_top"
14
- data-controller="refresh"
15
- data-refresh-interval-value="<%= fd_chart_poll_ms %>"
16
- data-refresh-url-value="<%= root_path(range: @window.key) %>">
8
+ <%= fd_refresh_frame "fd-throughput", url: root_path(range: @window.key), interval: fd_chart_poll_ms do %>
17
9
  <%= render "flightdeck/overview/throughput", series: @series, window: @window %>
18
- </turbo-frame>
10
+ <% end %>
19
11
 
20
- <turbo-frame id="fd-completion"
21
- target="_top"
22
- data-controller="refresh"
23
- data-refresh-interval-value="<%= fd_chart_poll_ms %>"
24
- data-refresh-url-value="<%= root_path(range: @window.key) %>">
12
+ <%= fd_refresh_frame "fd-completion", url: root_path(range: @window.key), interval: fd_chart_poll_ms do %>
25
13
  <%= render "flightdeck/overview/completion", series: @series, window: @window %>
26
- </turbo-frame>
14
+ <% end %>
27
15
  </div>
28
16
 
29
17
  <div class="fd-grid-2b">
30
- <turbo-frame id="fd-overview-queues"
31
- target="_top"
32
- data-controller="refresh"
33
- data-refresh-interval-value="<%= fd_poll_ms %>"
34
- data-refresh-url-value="<%= root_path(range: @window.key) %>">
18
+ <%= fd_refresh_frame "fd-overview-queues", url: root_path(range: @window.key) do %>
35
19
  <%= render "flightdeck/overview/queues", overview: @overview %>
36
- </turbo-frame>
20
+ <% end %>
37
21
 
38
- <turbo-frame id="fd-overview-failures"
39
- target="_top"
40
- data-controller="refresh"
41
- data-refresh-interval-value="<%= fd_poll_ms %>"
42
- data-refresh-url-value="<%= root_path(range: @window.key) %>">
22
+ <%= fd_refresh_frame "fd-overview-failures", url: root_path(range: @window.key) do %>
43
23
  <%= render "flightdeck/overview/failures", overview: @overview %>
44
- </turbo-frame>
24
+ <% end %>
45
25
  </div>
46
26
 
47
- <turbo-frame id="fd-fleet"
48
- target="_top"
49
- class="fd-block"
50
- data-controller="refresh"
51
- data-refresh-interval-value="<%= fd_poll_ms %>"
52
- data-refresh-url-value="<%= root_path(range: @window.key) %>">
27
+ <%= fd_refresh_frame "fd-fleet", url: root_path(range: @window.key), class: "fd-block" do %>
53
28
  <%= render "flightdeck/overview/fleet", registry: @overview.registry %>
54
- </turbo-frame>
29
+ <% end %>
@@ -1,9 +1,5 @@
1
1
  <% content_for :page_title, "Processes" %>
2
2
 
3
- <turbo-frame id="fd-processes"
4
- target="_top"
5
- data-controller="refresh"
6
- data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
7
- data-refresh-url-value="<%= processes_path %>">
3
+ <%= fd_refresh_frame "fd-processes", url: processes_path do %>
8
4
  <%= render "flightdeck/processes/fleet", registry: @registry %>
9
- </turbo-frame>
5
+ <% end %>
@@ -1,9 +1,5 @@
1
1
  <% content_for :page_title, "Queues" %>
2
2
 
3
- <turbo-frame id="fd-queues"
4
- target="_top"
5
- data-controller="refresh"
6
- data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
7
- data-refresh-url-value="<%= queues_path %>">
3
+ <%= fd_refresh_frame "fd-queues", url: queues_path do %>
8
4
  <%= render "flightdeck/queues/cards", stats: @stats, sparklines: @sparklines %>
9
- </turbo-frame>
5
+ <% end %>
@@ -1,9 +1,5 @@
1
1
  <% content_for :page_title, "Recurring tasks" %>
2
2
 
3
- <turbo-frame id="fd-recurring"
4
- target="_top"
5
- data-controller="refresh"
6
- data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
7
- data-refresh-url-value="<%= recurring_tasks_path %>">
3
+ <%= fd_refresh_frame "fd-recurring", url: recurring_tasks_path do %>
8
4
  <%= render "flightdeck/recurring_tasks/table", catalog: @catalog %>
9
- </turbo-frame>
5
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <turbo-stream action="append" target="fd-toasts">
2
+ <template><%= render partial: "flightdeck/shared/toast", formats: [ :html ], locals: { toast: @toast } %></template>
3
+ </turbo-stream>
4
+
5
+ <%# Re-render the frame the action was launched from, so the same round trip
6
+ that carries the toast also carries settled rows and counts. A page that
7
+ does not have the frame (the job detail page, say) simply ignores this. %>
8
+ <turbo-stream action="replace" target="<%= @frame[:id] %>">
9
+ <template>
10
+ <%= fd_refresh_frame @frame[:id], url: @frame[:url], target: @frame.fetch(:target, "_top") do %>
11
+ <%= render partial: @frame[:partial], formats: [ :html ], locals: @frame[:locals] %>
12
+ <% end %>
13
+ </template>
14
+ </turbo-stream>
@@ -58,12 +58,6 @@ module Flightdeck
58
58
  CONTENT_TYPES.fetch(File.extname(file), "application/octet-stream")
59
59
  end
60
60
 
61
- def reload!
62
- remove_instance_variable(:@manifest) if defined?(@manifest)
63
- remove_instance_variable(:@by_digested_name) if defined?(@by_digested_name)
64
- manifest
65
- end
66
-
67
61
  private
68
62
  def load_manifest
69
63
  path = root.join("manifest.json")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flightdeck
4
- VERSION = "0.5.2"
4
+ VERSION = "0.5.3"
5
5
  end