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.
- checksums.yaml +4 -4
- data/app/assets/flightdeck/{flightdeck-00403457c5d9.css → flightdeck-3967b9e6c506.css} +1 -1
- data/app/assets/flightdeck/{flightdeck-747ab6d1a6fa.js → flightdeck-3aac23f80afe.js} +2 -2
- data/app/assets/flightdeck/manifest.json +7 -7
- data/app/controllers/concerns/flightdeck/toasts.rb +23 -13
- data/app/controllers/flightdeck/jobs/actions_controller.rb +41 -42
- data/app/controllers/flightdeck/jobs/discards_controller.rb +0 -1
- data/app/controllers/flightdeck/jobs/retries_controller.rb +0 -1
- data/app/controllers/flightdeck/processes_controller.rb +8 -3
- data/app/controllers/flightdeck/queues_controller.rb +8 -3
- data/app/controllers/flightdeck/recurring_tasks_controller.rb +7 -2
- data/app/helpers/flightdeck/application_helper.rb +17 -0
- data/app/helpers/flightdeck/jobs_helper.rb +1 -20
- data/app/models/flightdeck/duration.rb +23 -0
- data/app/models/flightdeck/job_detail.rb +1 -8
- data/app/models/flightdeck/job_row.rb +10 -0
- data/app/models/flightdeck/jobs_query.rb +8 -16
- data/app/models/flightdeck/metrics/line_chart.rb +3 -7
- data/app/models/flightdeck/overview.rb +12 -19
- data/app/models/flightdeck/process_registry.rb +0 -6
- data/app/models/flightdeck/queue_stats.rb +0 -2
- data/app/models/flightdeck/recurring_catalog.rb +0 -2
- data/app/views/flightdeck/jobs/_list.html.erb +9 -4
- data/app/views/flightdeck/jobs/index.html.erb +2 -5
- data/app/views/flightdeck/overview/_tiles.html.erb +1 -1
- data/app/views/flightdeck/overview/index.html.erb +12 -37
- data/app/views/flightdeck/processes/index.html.erb +2 -6
- data/app/views/flightdeck/queues/index.html.erb +2 -6
- data/app/views/flightdeck/recurring_tasks/index.html.erb +2 -6
- data/app/views/flightdeck/shared/refresh.turbo_stream.erb +14 -0
- data/lib/flightdeck/assets.rb +0 -6
- data/lib/flightdeck/version.rb +1 -1
- metadata +5 -7
- data/app/views/flightdeck/jobs/actions/create.turbo_stream.erb +0 -18
- data/app/views/flightdeck/processes/prune.turbo_stream.erb +0 -15
- data/app/views/flightdeck/queues/update.turbo_stream.erb +0 -15
- data/app/views/flightdeck/recurring_tasks/run.turbo_stream.erb +0 -15
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
128
|
-
#
|
|
129
|
+
# Never `SELECT *`: it would drag a payload column through every list
|
|
130
|
+
# query.
|
|
129
131
|
def driving_columns
|
|
130
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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 #{
|
|
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 ? "/ #{
|
|
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 ?
|
|
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)
|
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5
|
+
<% end %>
|
|
10
6
|
|
|
11
7
|
<div class="fd-grid-2">
|
|
12
|
-
|
|
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
|
-
|
|
10
|
+
<% end %>
|
|
19
11
|
|
|
20
|
-
|
|
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
|
-
|
|
14
|
+
<% end %>
|
|
27
15
|
</div>
|
|
28
16
|
|
|
29
17
|
<div class="fd-grid-2b">
|
|
30
|
-
|
|
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
|
-
|
|
20
|
+
<% end %>
|
|
37
21
|
|
|
38
|
-
|
|
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
|
-
|
|
24
|
+
<% end %>
|
|
45
25
|
</div>
|
|
46
26
|
|
|
47
|
-
|
|
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
|
-
|
|
29
|
+
<% end %>
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
<% content_for :page_title, "Processes" %>
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
<% end %>
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
<% content_for :page_title, "Queues" %>
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
<% end %>
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
<% content_for :page_title, "Recurring tasks" %>
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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>
|
data/lib/flightdeck/assets.rb
CHANGED
|
@@ -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")
|
data/lib/flightdeck/version.rb
CHANGED