solid_queue_tui 0.1.2 → 0.2.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.
- checksums.yaml +4 -4
- data/exe/qtop +35 -0
- data/exe/sqtui +19 -2
- data/lib/solid_queue_tui/application.rb +37 -4
- data/lib/solid_queue_tui/cli.rb +2 -2
- data/lib/solid_queue_tui/components/job_table.rb +2 -3
- data/lib/solid_queue_tui/data/hourly_stats_query.rb +85 -0
- data/lib/solid_queue_tui/data/jobs_query.rb +31 -0
- data/lib/solid_queue_tui/data/processes_query.rb +23 -0
- data/lib/solid_queue_tui/data/stats.rb +15 -3
- data/lib/solid_queue_tui/formatting_helpers.rb +63 -0
- data/lib/solid_queue_tui/version.rb +1 -1
- data/lib/solid_queue_tui/views/blocked_view.rb +9 -77
- data/lib/solid_queue_tui/views/concerns/confirmable.rb +53 -0
- data/lib/solid_queue_tui/views/concerns/filterable.rb +4 -0
- data/lib/solid_queue_tui/views/concerns/paginatable.rb +79 -0
- data/lib/solid_queue_tui/views/dashboard_view.rb +228 -22
- data/lib/solid_queue_tui/views/failed_view.rb +49 -152
- data/lib/solid_queue_tui/views/finished_view.rb +12 -80
- data/lib/solid_queue_tui/views/in_progress_view.rb +9 -72
- data/lib/solid_queue_tui/views/job_detail_view.rb +179 -31
- data/lib/solid_queue_tui/views/processes_view.rb +2 -24
- data/lib/solid_queue_tui/views/queues_view.rb +226 -87
- data/lib/solid_queue_tui/views/recurring_tasks_view.rb +22 -69
- data/lib/solid_queue_tui/views/scheduled_view.rb +39 -142
- data/lib/solid_queue_tui.rb +4 -0
- metadata +9 -3
|
@@ -116,6 +116,10 @@ module SolidQueueTui
|
|
|
116
116
|
"#{base_title} (#{parts.join(', ')})"
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
def clear_filter_binding
|
|
120
|
+
@filters.empty? ? nil : { key: "c", action: "Clear Filter" }
|
|
121
|
+
end
|
|
122
|
+
|
|
119
123
|
def filter_bindings
|
|
120
124
|
[
|
|
121
125
|
{ key: "Tab", action: "Next Field" },
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
module Paginatable
|
|
6
|
+
LOAD_THRESHOLD = 10
|
|
7
|
+
|
|
8
|
+
def init_pagination
|
|
9
|
+
@table_state = RatatuiRuby::TableState.new(nil)
|
|
10
|
+
@table_state.select(0)
|
|
11
|
+
@selected_row = 0
|
|
12
|
+
@items = []
|
|
13
|
+
@total_count = nil
|
|
14
|
+
@all_loaded = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def items = @items
|
|
18
|
+
|
|
19
|
+
def selected_item
|
|
20
|
+
return nil if @items.empty? || @selected_row >= @items.size
|
|
21
|
+
@items[@selected_row]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def total_count=(count)
|
|
25
|
+
@total_count = count
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def current_offset
|
|
29
|
+
@items.size
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset_pagination!
|
|
33
|
+
@items = []
|
|
34
|
+
@total_count = nil
|
|
35
|
+
@all_loaded = false
|
|
36
|
+
@selected_row = 0
|
|
37
|
+
@table_state.select(0)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def update_items(new_items)
|
|
43
|
+
@selected_row = 0 if @selected_row >= new_items.size
|
|
44
|
+
@items = new_items
|
|
45
|
+
@all_loaded = new_items.size < SolidQueueTui.page_size
|
|
46
|
+
@selected_row = @selected_row.clamp(0, [@items.size - 1, 0].max)
|
|
47
|
+
@table_state.select(@selected_row)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def append_items(more_items)
|
|
51
|
+
@items.concat(more_items)
|
|
52
|
+
@all_loaded = more_items.size < SolidQueueTui.page_size
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def needs_more?
|
|
56
|
+
!@all_loaded && @selected_row >= @items.size - LOAD_THRESHOLD
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def move_selection(delta)
|
|
60
|
+
return if @items.empty?
|
|
61
|
+
@selected_row = (@selected_row + delta).clamp(0, @items.size - 1)
|
|
62
|
+
@table_state.select(@selected_row)
|
|
63
|
+
:load_more if needs_more?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def jump_to_top
|
|
67
|
+
@selected_row = 0
|
|
68
|
+
@table_state.select(0)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def jump_to_bottom
|
|
72
|
+
return if @items.empty?
|
|
73
|
+
@selected_row = @items.size - 1
|
|
74
|
+
@table_state.select(@selected_row)
|
|
75
|
+
:load_more if needs_more?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class DashboardView
|
|
6
|
+
include FormattingHelpers
|
|
7
|
+
|
|
6
8
|
def initialize(tui)
|
|
7
9
|
@tui = tui
|
|
8
|
-
@selected_row = 0
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def update(stats:)
|
|
@@ -23,7 +24,7 @@ module SolidQueueTui
|
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
render_overview_panels(frame, top)
|
|
26
|
-
|
|
27
|
+
render_metrics(frame, bottom)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def handle_input(event)
|
|
@@ -45,6 +46,8 @@ module SolidQueueTui
|
|
|
45
46
|
|
|
46
47
|
private
|
|
47
48
|
|
|
49
|
+
# --- Top panels (sticky) ---
|
|
50
|
+
|
|
48
51
|
def render_overview_panels(frame, area)
|
|
49
52
|
return unless @stats
|
|
50
53
|
|
|
@@ -127,28 +130,228 @@ module SolidQueueTui
|
|
|
127
130
|
)
|
|
128
131
|
end
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
# --- Bottom section: chart + queue summary ---
|
|
134
|
+
|
|
135
|
+
def render_metrics(frame, area)
|
|
131
136
|
return unless @stats
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
@tui.
|
|
138
|
-
@tui.
|
|
138
|
+
chart_area, bottom_area = @tui.layout_split(
|
|
139
|
+
area,
|
|
140
|
+
direction: :vertical,
|
|
141
|
+
constraints: [
|
|
142
|
+
@tui.constraint_percentage(70),
|
|
143
|
+
@tui.constraint_percentage(30)
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
queue_area, summary_area = @tui.layout_split(
|
|
148
|
+
bottom_area,
|
|
149
|
+
direction: :horizontal,
|
|
150
|
+
constraints: [
|
|
151
|
+
@tui.constraint_percentage(50),
|
|
152
|
+
@tui.constraint_percentage(50)
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
render_throughput_chart(frame, chart_area)
|
|
157
|
+
render_queue_depth(frame, queue_area)
|
|
158
|
+
render_summary(frame, summary_area)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def render_throughput_chart(frame, area)
|
|
162
|
+
enqueued = @stats.enqueued_per_hour
|
|
163
|
+
processed = @stats.processed_per_hour
|
|
164
|
+
failed = @stats.failed_per_hour
|
|
165
|
+
|
|
166
|
+
# Convert 24-element arrays to [x, y] coordinate pairs
|
|
167
|
+
enqueued_data = to_chart_data(enqueued)
|
|
168
|
+
processed_data = to_chart_data(processed)
|
|
169
|
+
failed_data = to_chart_data(failed)
|
|
170
|
+
|
|
171
|
+
# Y-axis bounds
|
|
172
|
+
all_values = [enqueued&.data, processed&.data, failed&.data].compact.flatten
|
|
173
|
+
y_max = (all_values.max || 10).to_f
|
|
174
|
+
y_max = 10.0 if y_max == 0
|
|
175
|
+
|
|
176
|
+
# X-axis labels at key positions
|
|
177
|
+
now = Time.now.utc
|
|
178
|
+
x_labels = [0, 6, 12, 18, 23].map do |i|
|
|
179
|
+
(now - (23 - i) * 3600).strftime("%H:%M")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Y-axis labels
|
|
183
|
+
y_labels = ["0", format_number((y_max / 2).round), format_number(y_max.round)]
|
|
184
|
+
|
|
185
|
+
datasets = []
|
|
186
|
+
if enqueued_data.any?
|
|
187
|
+
datasets << RatatuiRuby::Widgets::Dataset.new(
|
|
188
|
+
name: "",
|
|
189
|
+
data: enqueued_data,
|
|
190
|
+
style: @tui.style(fg: :cyan),
|
|
191
|
+
marker: :braille,
|
|
192
|
+
graph_type: :line
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if processed_data.any?
|
|
197
|
+
datasets << RatatuiRuby::Widgets::Dataset.new(
|
|
198
|
+
name: "",
|
|
199
|
+
data: processed_data,
|
|
200
|
+
style: @tui.style(fg: :green),
|
|
201
|
+
marker: :braille,
|
|
202
|
+
graph_type: :line
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if failed_data.any?
|
|
207
|
+
datasets << RatatuiRuby::Widgets::Dataset.new(
|
|
208
|
+
name: "",
|
|
209
|
+
data: failed_data,
|
|
210
|
+
style: @tui.style(fg: :red),
|
|
211
|
+
marker: :braille,
|
|
212
|
+
graph_type: :line
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
x_axis = RatatuiRuby::Widgets::Axis.new(
|
|
217
|
+
bounds: [0.0, 23.0],
|
|
218
|
+
labels: x_labels,
|
|
219
|
+
style: @tui.style(fg: :dark_gray)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
y_axis = RatatuiRuby::Widgets::Axis.new(
|
|
223
|
+
bounds: [0.0, y_max],
|
|
224
|
+
labels: y_labels,
|
|
225
|
+
style: @tui.style(fg: :dark_gray)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
chart = @tui.chart(
|
|
229
|
+
datasets: datasets,
|
|
230
|
+
x_axis: x_axis,
|
|
231
|
+
y_axis: y_axis,
|
|
232
|
+
block: @tui.block(
|
|
233
|
+
title: " Throughput (24h) ",
|
|
234
|
+
title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
|
|
235
|
+
titles: [
|
|
236
|
+
{ content: " ● Enqueued ", position: :top, alignment: :right, style: @tui.style(fg: :cyan) },
|
|
237
|
+
{ content: "● Processed ", position: :top, alignment: :right, style: @tui.style(fg: :green) },
|
|
238
|
+
{ content: "● Failed ", position: :top, alignment: :right, style: @tui.style(fg: :red) }
|
|
239
|
+
],
|
|
240
|
+
borders: [:all],
|
|
241
|
+
border_type: :rounded,
|
|
242
|
+
border_style: @tui.style(fg: :dark_gray)
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
frame.render_widget(chart, area)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def render_queue_depth(frame, area)
|
|
250
|
+
lines = []
|
|
251
|
+
|
|
252
|
+
queue_depths = @stats.queue_depths
|
|
253
|
+
if queue_depths.any?
|
|
254
|
+
total_depth = queue_depths.values.sum
|
|
255
|
+
sorted = queue_depths.sort_by { |_, v| -v }
|
|
256
|
+
top_queues = sorted.first(5)
|
|
257
|
+
remaining = sorted.drop(5)
|
|
258
|
+
max_depth = top_queues.first&.last || 1
|
|
259
|
+
|
|
260
|
+
top_queues.each_with_index do |(name, count), idx|
|
|
261
|
+
pct = total_depth > 0 ? (count.to_f / total_depth * 100).round(1) : 0
|
|
262
|
+
bar_width = 20
|
|
263
|
+
filled = max_depth > 0 ? (count.to_f / max_depth * bar_width).round : 0
|
|
264
|
+
empty_bar = bar_width - filled
|
|
265
|
+
|
|
266
|
+
lines << @tui.text_line(spans: [
|
|
267
|
+
@tui.text_span(content: " #{name.ljust(14)}", style: @tui.style(fg: :white)),
|
|
268
|
+
@tui.text_span(content: "#{"█" * filled}", style: @tui.style(fg: :cyan)),
|
|
269
|
+
@tui.text_span(content: "#{"░" * empty_bar}", style: @tui.style(fg: :dark_gray)),
|
|
270
|
+
@tui.text_span(content: " #{format_number(count).rjust(6)} (#{pct}%)", style: @tui.style(fg: :dark_gray))
|
|
271
|
+
])
|
|
272
|
+
|
|
273
|
+
if idx < top_queues.size - 1
|
|
274
|
+
lines << @tui.text_line(spans: [
|
|
275
|
+
@tui.text_span(content: "", style: @tui.style(fg: :dark_gray))
|
|
276
|
+
])
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
if remaining.any?
|
|
281
|
+
others_count = remaining.sum { |_, v| v }
|
|
282
|
+
pct = total_depth > 0 ? (others_count.to_f / total_depth * 100).round(1) : 0
|
|
283
|
+
lines << @tui.text_line(spans: [
|
|
284
|
+
@tui.text_span(content: " +#{remaining.size} more", style: @tui.style(fg: :dark_gray)),
|
|
285
|
+
@tui.text_span(content: " #{format_number(others_count).rjust(26)} (#{pct}%)", style: @tui.style(fg: :dark_gray))
|
|
286
|
+
])
|
|
287
|
+
end
|
|
288
|
+
else
|
|
289
|
+
lines << @tui.text_line(spans: [
|
|
290
|
+
@tui.text_span(content: " No queued jobs", style: @tui.style(fg: :dark_gray))
|
|
139
291
|
])
|
|
140
|
-
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
frame.render_widget(
|
|
295
|
+
@tui.paragraph(
|
|
296
|
+
text: lines,
|
|
297
|
+
block: @tui.block(
|
|
298
|
+
title: " Queue Depth ",
|
|
299
|
+
title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
|
|
300
|
+
borders: [:all],
|
|
301
|
+
border_type: :rounded,
|
|
302
|
+
border_style: @tui.style(fg: :dark_gray)
|
|
303
|
+
)
|
|
304
|
+
),
|
|
305
|
+
area
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def render_summary(frame, area)
|
|
310
|
+
lines = []
|
|
311
|
+
|
|
312
|
+
# Throughput totals (24h)
|
|
313
|
+
enq_total = @stats.enqueued_per_hour&.total || 0
|
|
314
|
+
proc_total = @stats.processed_per_hour&.total || 0
|
|
315
|
+
fail_total = @stats.failed_per_hour&.total || 0
|
|
316
|
+
|
|
317
|
+
[
|
|
318
|
+
["Enqueued", enq_total, :cyan],
|
|
319
|
+
["Processed", proc_total, :green],
|
|
320
|
+
["Failed", fail_total, :red]
|
|
321
|
+
].each do |label, value, color|
|
|
322
|
+
lines << @tui.text_line(spans: [
|
|
323
|
+
@tui.text_span(content: " #{label.ljust(12)}", style: @tui.style(fg: color)),
|
|
324
|
+
@tui.text_span(content: format_number(value).rjust(10), style: @tui.style(fg: :white, modifiers: [:bold]))
|
|
325
|
+
])
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Separator
|
|
329
|
+
lines << @tui.text_line(spans: [
|
|
330
|
+
@tui.text_span(content: "", style: @tui.style(fg: :dark_gray))
|
|
331
|
+
])
|
|
332
|
+
|
|
333
|
+
# Overall totals + completion bar
|
|
334
|
+
[
|
|
335
|
+
["Total", @stats.total_jobs, :white],
|
|
336
|
+
["Completed", @stats.completed_jobs, :green]
|
|
337
|
+
].each do |label, value, color|
|
|
338
|
+
lines << @tui.text_line(spans: [
|
|
339
|
+
@tui.text_span(content: " #{label.ljust(12)}", style: @tui.style(fg: :dark_gray)),
|
|
340
|
+
@tui.text_span(content: format_number(value).rjust(10), style: @tui.style(fg: color, modifiers: [:bold]))
|
|
341
|
+
])
|
|
342
|
+
end
|
|
141
343
|
|
|
142
344
|
if @stats.total_jobs > 0
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
filled = (
|
|
146
|
-
|
|
345
|
+
ratio = @stats.completed_jobs.to_f / @stats.total_jobs
|
|
346
|
+
bar_w = 30
|
|
347
|
+
filled = (ratio * bar_w).round
|
|
348
|
+
empty_bar = bar_w - filled
|
|
147
349
|
|
|
148
350
|
lines << @tui.text_line(spans: [
|
|
149
|
-
@tui.text_span(content: "
|
|
150
|
-
@tui.text_span(content: "#{
|
|
151
|
-
@tui.text_span(content: "
|
|
351
|
+
@tui.text_span(content: " ", style: @tui.style(fg: :dark_gray)),
|
|
352
|
+
@tui.text_span(content: "#{"█" * filled}", style: @tui.style(fg: :green)),
|
|
353
|
+
@tui.text_span(content: "#{"░" * empty_bar}", style: @tui.style(fg: :dark_gray)),
|
|
354
|
+
@tui.text_span(content: " #{(ratio * 100).round(1)}%", style: @tui.style(fg: :white))
|
|
152
355
|
])
|
|
153
356
|
end
|
|
154
357
|
|
|
@@ -156,7 +359,7 @@ module SolidQueueTui
|
|
|
156
359
|
@tui.paragraph(
|
|
157
360
|
text: lines,
|
|
158
361
|
block: @tui.block(
|
|
159
|
-
title: "
|
|
362
|
+
title: " Summary ",
|
|
160
363
|
title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
|
|
161
364
|
borders: [:all],
|
|
162
365
|
border_type: :rounded,
|
|
@@ -167,6 +370,13 @@ module SolidQueueTui
|
|
|
167
370
|
)
|
|
168
371
|
end
|
|
169
372
|
|
|
373
|
+
# --- Helpers ---
|
|
374
|
+
|
|
375
|
+
def to_chart_data(result)
|
|
376
|
+
return [] unless result
|
|
377
|
+
result.data.each_with_index.map { |v, i| [i.to_f, v.to_f] }
|
|
378
|
+
end
|
|
379
|
+
|
|
170
380
|
def status_line(label, value, color)
|
|
171
381
|
bar_char = value.to_i > 0 ? "●" : "○"
|
|
172
382
|
@tui.text_line(spans: [
|
|
@@ -179,10 +389,6 @@ module SolidQueueTui
|
|
|
179
389
|
])
|
|
180
390
|
end
|
|
181
391
|
|
|
182
|
-
def format_number(n)
|
|
183
|
-
return "0" if n.nil? || n == 0
|
|
184
|
-
n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
185
|
-
end
|
|
186
392
|
end
|
|
187
393
|
end
|
|
188
394
|
end
|
|
@@ -4,62 +4,29 @@ module SolidQueueTui
|
|
|
4
4
|
module Views
|
|
5
5
|
class FailedView
|
|
6
6
|
include Filterable
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
include Confirmable
|
|
8
|
+
include Paginatable
|
|
9
|
+
include FormattingHelpers
|
|
10
10
|
|
|
11
11
|
def initialize(tui)
|
|
12
12
|
@tui = tui
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@selected_row = 0
|
|
16
|
-
@failed_jobs = []
|
|
17
|
-
@total_count = nil
|
|
18
|
-
@all_loaded = false
|
|
19
|
-
@confirm_action = nil
|
|
13
|
+
init_pagination
|
|
14
|
+
init_confirm
|
|
20
15
|
init_filter
|
|
21
16
|
end
|
|
22
17
|
|
|
23
18
|
def update(failed_jobs:)
|
|
24
|
-
|
|
25
|
-
@all_loaded = failed_jobs.size < SolidQueueTui.page_size
|
|
26
|
-
@selected_row = @selected_row.clamp(0, [@failed_jobs.size - 1, 0].max)
|
|
27
|
-
@table_state.select(@selected_row)
|
|
19
|
+
update_items(failed_jobs)
|
|
28
20
|
end
|
|
29
21
|
|
|
30
22
|
def append(failed_jobs:)
|
|
31
|
-
|
|
32
|
-
@all_loaded = failed_jobs.size < SolidQueueTui.page_size
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def total_count=(count)
|
|
36
|
-
@total_count = count
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def current_offset
|
|
40
|
-
@failed_jobs.size
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def reset_pagination!
|
|
44
|
-
@failed_jobs = []
|
|
45
|
-
@total_count = nil
|
|
46
|
-
@all_loaded = false
|
|
47
|
-
@selected_row = 0
|
|
48
|
-
@table_state.select(0)
|
|
23
|
+
append_items(failed_jobs)
|
|
49
24
|
end
|
|
50
25
|
|
|
51
26
|
def render(frame, area)
|
|
52
|
-
if
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
direction: :vertical,
|
|
56
|
-
constraints: [
|
|
57
|
-
@tui.constraint_length(3),
|
|
58
|
-
@tui.constraint_fill(1)
|
|
59
|
-
]
|
|
60
|
-
)
|
|
61
|
-
render_confirm(frame, confirm_area)
|
|
62
|
-
render_failed_table(frame, content_area)
|
|
27
|
+
if confirm_mode?
|
|
28
|
+
render_failed_table(frame, area)
|
|
29
|
+
render_confirm_popup(frame, area)
|
|
63
30
|
elsif filter_mode?
|
|
64
31
|
filter_area, content_area = @tui.layout_split(
|
|
65
32
|
area,
|
|
@@ -77,7 +44,7 @@ module SolidQueueTui
|
|
|
77
44
|
end
|
|
78
45
|
|
|
79
46
|
def handle_input(event)
|
|
80
|
-
if
|
|
47
|
+
if confirm_mode?
|
|
81
48
|
handle_confirm_input(event)
|
|
82
49
|
elsif filter_mode?
|
|
83
50
|
handle_filter_input(event)
|
|
@@ -86,17 +53,9 @@ module SolidQueueTui
|
|
|
86
53
|
end
|
|
87
54
|
end
|
|
88
55
|
|
|
89
|
-
def selected_item
|
|
90
|
-
return nil if @failed_jobs.empty? || @selected_row >= @failed_jobs.size
|
|
91
|
-
@failed_jobs[@selected_row]
|
|
92
|
-
end
|
|
93
|
-
|
|
94
56
|
def bindings
|
|
95
|
-
if
|
|
96
|
-
|
|
97
|
-
{ key: "y", action: "Confirm" },
|
|
98
|
-
{ key: "n/Esc", action: "Cancel" }
|
|
99
|
-
]
|
|
57
|
+
if confirm_mode?
|
|
58
|
+
confirm_bindings
|
|
100
59
|
elsif filter_mode?
|
|
101
60
|
filter_bindings
|
|
102
61
|
else
|
|
@@ -106,13 +65,14 @@ module SolidQueueTui
|
|
|
106
65
|
{ key: "R", action: "Retry" },
|
|
107
66
|
{ key: "D", action: "Discard" },
|
|
108
67
|
{ key: "A", action: "Retry All" },
|
|
109
|
-
{ key: "/", action: "Filter" }
|
|
110
|
-
|
|
68
|
+
{ key: "/", action: "Filter" },
|
|
69
|
+
clear_filter_binding
|
|
70
|
+
].compact
|
|
111
71
|
end
|
|
112
72
|
end
|
|
113
73
|
|
|
114
74
|
def capturing_input?
|
|
115
|
-
filter_mode? ||
|
|
75
|
+
filter_mode? || confirm_mode?
|
|
116
76
|
end
|
|
117
77
|
|
|
118
78
|
def breadcrumb
|
|
@@ -121,10 +81,6 @@ module SolidQueueTui
|
|
|
121
81
|
|
|
122
82
|
private
|
|
123
83
|
|
|
124
|
-
def needs_more?
|
|
125
|
-
!@all_loaded && @selected_row >= @failed_jobs.size - LOAD_THRESHOLD
|
|
126
|
-
end
|
|
127
|
-
|
|
128
84
|
def handle_normal_input(event)
|
|
129
85
|
case event
|
|
130
86
|
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
@@ -144,64 +100,50 @@ module SolidQueueTui
|
|
|
144
100
|
@confirm_action = :discard if selected_item
|
|
145
101
|
nil
|
|
146
102
|
in { type: :key, code: "A" }
|
|
147
|
-
@confirm_action = :retry_all unless
|
|
103
|
+
@confirm_action = :retry_all unless items.empty?
|
|
148
104
|
nil
|
|
149
105
|
in { type: :key, code: "/" }
|
|
150
106
|
enter_filter_mode
|
|
151
107
|
nil
|
|
152
|
-
in { type: :key, code: "
|
|
108
|
+
in { type: :key, code: "c" }
|
|
153
109
|
clear_filter
|
|
154
110
|
else
|
|
155
111
|
nil
|
|
156
112
|
end
|
|
157
113
|
end
|
|
158
114
|
|
|
159
|
-
def
|
|
160
|
-
case
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
item = selected_item
|
|
172
|
-
return nil unless item
|
|
173
|
-
Actions::DiscardJob.call(item.id)
|
|
174
|
-
:refresh
|
|
175
|
-
when :retry_all
|
|
176
|
-
f = filters
|
|
177
|
-
Actions::RetryJob.retry_all(filter: f[:class_name], queue: f[:queue])
|
|
178
|
-
:refresh
|
|
179
|
-
end
|
|
180
|
-
in { type: :key, code: "n" } | { type: :key, code: "esc" }
|
|
181
|
-
@confirm_action = nil
|
|
182
|
-
nil
|
|
183
|
-
else
|
|
184
|
-
nil
|
|
115
|
+
def confirm_message
|
|
116
|
+
case @confirm_action
|
|
117
|
+
when :retry
|
|
118
|
+
job = selected_item
|
|
119
|
+
"Retry job ##{job&.job_id} (#{job&.class_name})? [y/n]"
|
|
120
|
+
when :discard
|
|
121
|
+
job = selected_item
|
|
122
|
+
"Discard job ##{job&.job_id} (#{job&.class_name})? This cannot be undone. [y/n]"
|
|
123
|
+
when :retry_all
|
|
124
|
+
count = @total_count || items.size
|
|
125
|
+
label = @filters.empty? ? "failed jobs" : "filtered failed jobs"
|
|
126
|
+
"Retry ALL #{count} #{label}? [y/n]"
|
|
185
127
|
end
|
|
186
128
|
end
|
|
187
129
|
|
|
188
|
-
def
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
130
|
+
def execute_confirm_action(action)
|
|
131
|
+
case action
|
|
132
|
+
when :retry
|
|
133
|
+
item = selected_item
|
|
134
|
+
return nil unless item
|
|
135
|
+
Actions::RetryJob.call(item.id)
|
|
136
|
+
:refresh
|
|
137
|
+
when :discard
|
|
138
|
+
item = selected_item
|
|
139
|
+
return nil unless item
|
|
140
|
+
Actions::DiscardJob.call(item.id)
|
|
141
|
+
:refresh
|
|
142
|
+
when :retry_all
|
|
143
|
+
f = filters
|
|
144
|
+
Actions::RetryJob.retry_all(filter: f[:class_name], queue: f[:queue])
|
|
145
|
+
:refresh
|
|
146
|
+
end
|
|
205
147
|
end
|
|
206
148
|
|
|
207
149
|
def render_failed_table(frame, area)
|
|
@@ -214,7 +156,7 @@ module SolidQueueTui
|
|
|
214
156
|
{ key: :failed_at, label: "FAILED", width: 12 }
|
|
215
157
|
]
|
|
216
158
|
|
|
217
|
-
rows =
|
|
159
|
+
rows = items.map do |job|
|
|
218
160
|
{
|
|
219
161
|
id: job.job_id,
|
|
220
162
|
class_name: job.class_name,
|
|
@@ -238,51 +180,6 @@ module SolidQueueTui
|
|
|
238
180
|
table.render(frame, area, @table_state)
|
|
239
181
|
end
|
|
240
182
|
|
|
241
|
-
def render_confirm(frame, area)
|
|
242
|
-
message = case @confirm_action
|
|
243
|
-
when :retry
|
|
244
|
-
job = selected_item
|
|
245
|
-
"Retry job ##{job&.job_id} (#{job&.class_name})? [y/n]"
|
|
246
|
-
when :discard
|
|
247
|
-
job = selected_item
|
|
248
|
-
"Discard job ##{job&.job_id} (#{job&.class_name})? This cannot be undone. [y/n]"
|
|
249
|
-
when :retry_all
|
|
250
|
-
count = @total_count || @failed_jobs.size
|
|
251
|
-
label = @filters.empty? ? "failed jobs" : "filtered failed jobs"
|
|
252
|
-
"Retry ALL #{count} #{label}? [y/n]"
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
frame.render_widget(
|
|
256
|
-
@tui.paragraph(
|
|
257
|
-
text: " #{message}",
|
|
258
|
-
style: @tui.style(fg: :yellow, modifiers: [:bold]),
|
|
259
|
-
block: @tui.block(
|
|
260
|
-
title: " Confirm ",
|
|
261
|
-
title_style: @tui.style(fg: :red, modifiers: [:bold]),
|
|
262
|
-
borders: [:all],
|
|
263
|
-
border_type: :rounded,
|
|
264
|
-
border_style: @tui.style(fg: :red)
|
|
265
|
-
)
|
|
266
|
-
),
|
|
267
|
-
area
|
|
268
|
-
)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def truncate(str, max)
|
|
272
|
-
return "" unless str
|
|
273
|
-
str.length > max ? "#{str[0...max - 3]}..." : str
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def time_ago(time)
|
|
277
|
-
return "n/a" unless time
|
|
278
|
-
seconds = (Time.now.utc - time).to_i
|
|
279
|
-
case seconds
|
|
280
|
-
when 0..59 then "#{seconds}s ago"
|
|
281
|
-
when 60..3599 then "#{seconds / 60}m ago"
|
|
282
|
-
when 3600..86399 then "#{seconds / 3600}h ago"
|
|
283
|
-
else "#{seconds / 86400}d ago"
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
183
|
end
|
|
287
184
|
end
|
|
288
185
|
end
|