solid_queue_tui 0.1.2 → 0.1.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/exe/sqtui +19 -2
- data/lib/solid_queue_tui/application.rb +36 -4
- data/lib/solid_queue_tui/components/job_table.rb +2 -3
- 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/formatting_helpers.rb +63 -0
- data/lib/solid_queue_tui/version.rb +1 -1
- data/lib/solid_queue_tui/views/blocked_view.rb +6 -75
- data/lib/solid_queue_tui/views/concerns/confirmable.rb +53 -0
- data/lib/solid_queue_tui/views/concerns/paginatable.rb +79 -0
- data/lib/solid_queue_tui/views/dashboard_view.rb +2 -4
- data/lib/solid_queue_tui/views/failed_view.rb +45 -149
- data/lib/solid_queue_tui/views/finished_view.rb +9 -77
- data/lib/solid_queue_tui/views/in_progress_view.rb +6 -70
- 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 +223 -87
- data/lib/solid_queue_tui/views/recurring_tasks_view.rb +22 -69
- data/lib/solid_queue_tui/views/scheduled_view.rb +36 -140
- data/lib/solid_queue_tui.rb +3 -0
- metadata +6 -3
|
@@ -3,16 +3,24 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class JobDetailView
|
|
6
|
+
include Confirmable
|
|
7
|
+
include FormattingHelpers
|
|
8
|
+
|
|
6
9
|
def initialize(tui)
|
|
7
10
|
@tui = tui
|
|
8
11
|
@job = nil
|
|
9
12
|
@failed_job = nil
|
|
13
|
+
@process = nil
|
|
14
|
+
@running_jobs = []
|
|
10
15
|
@scroll_offset = 0
|
|
16
|
+
init_confirm
|
|
11
17
|
end
|
|
12
18
|
|
|
13
|
-
def show(job: nil, failed_job: nil)
|
|
19
|
+
def show(job: nil, failed_job: nil, process: nil, running_jobs: [])
|
|
14
20
|
@job = job
|
|
15
21
|
@failed_job = failed_job
|
|
22
|
+
@process = process
|
|
23
|
+
@running_jobs = running_jobs
|
|
16
24
|
@scroll_offset = 0
|
|
17
25
|
@active = true
|
|
18
26
|
end
|
|
@@ -21,6 +29,9 @@ module SolidQueueTui
|
|
|
21
29
|
@active = false
|
|
22
30
|
@job = nil
|
|
23
31
|
@failed_job = nil
|
|
32
|
+
@process = nil
|
|
33
|
+
@running_jobs = []
|
|
34
|
+
@confirm_action = nil
|
|
24
35
|
end
|
|
25
36
|
|
|
26
37
|
def active? = @active
|
|
@@ -35,12 +46,60 @@ module SolidQueueTui
|
|
|
35
46
|
|
|
36
47
|
if @failed_job
|
|
37
48
|
render_failed_detail(frame, inner)
|
|
49
|
+
elsif @process
|
|
50
|
+
render_process_detail(frame, inner)
|
|
38
51
|
elsif @job
|
|
39
52
|
render_job_detail(frame, inner)
|
|
40
53
|
end
|
|
54
|
+
|
|
55
|
+
render_confirm_popup(frame, area) if confirm_mode?
|
|
41
56
|
end
|
|
42
57
|
|
|
43
58
|
def handle_input(event)
|
|
59
|
+
if confirm_mode?
|
|
60
|
+
handle_confirm_input(event)
|
|
61
|
+
else
|
|
62
|
+
handle_normal_input(event)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def bindings
|
|
67
|
+
if confirm_mode?
|
|
68
|
+
confirm_bindings
|
|
69
|
+
else
|
|
70
|
+
bindings = [
|
|
71
|
+
{ key: "Esc", action: "Close" },
|
|
72
|
+
{ key: "j/k", action: "Scroll" }
|
|
73
|
+
]
|
|
74
|
+
if @failed_job
|
|
75
|
+
bindings += [
|
|
76
|
+
{ key: "R", action: "Retry" },
|
|
77
|
+
{ key: "D", action: "Discard" }
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
bindings
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def capturing_input?
|
|
85
|
+
confirm_mode?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def breadcrumb
|
|
89
|
+
if @failed_job
|
|
90
|
+
"failed:#{@failed_job.job_id}"
|
|
91
|
+
elsif @process
|
|
92
|
+
"process:#{@process.id}"
|
|
93
|
+
elsif @job
|
|
94
|
+
"jobs:#{@job.id}"
|
|
95
|
+
else
|
|
96
|
+
"detail"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def handle_normal_input(event)
|
|
44
103
|
case event
|
|
45
104
|
in { type: :key, code: "esc" } | { type: :key, code: "q" }
|
|
46
105
|
hide
|
|
@@ -52,48 +111,38 @@ module SolidQueueTui
|
|
|
52
111
|
@scroll_offset += 1
|
|
53
112
|
nil
|
|
54
113
|
in { type: :key, code: "R" }
|
|
55
|
-
if @failed_job
|
|
56
|
-
|
|
57
|
-
hide
|
|
58
|
-
:refresh
|
|
59
|
-
end
|
|
114
|
+
@confirm_action = :retry if @failed_job
|
|
115
|
+
nil
|
|
60
116
|
in { type: :key, code: "D" }
|
|
61
|
-
if @failed_job
|
|
62
|
-
|
|
63
|
-
hide
|
|
64
|
-
:refresh
|
|
65
|
-
end
|
|
117
|
+
@confirm_action = :discard if @failed_job
|
|
118
|
+
nil
|
|
66
119
|
else
|
|
67
120
|
nil
|
|
68
121
|
end
|
|
69
122
|
end
|
|
70
123
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
bindings += [
|
|
78
|
-
{ key: "R", action: "Retry" },
|
|
79
|
-
{ key: "D", action: "Discard" }
|
|
80
|
-
]
|
|
124
|
+
def confirm_message
|
|
125
|
+
case @confirm_action
|
|
126
|
+
when :retry
|
|
127
|
+
"Retry job ##{@failed_job&.job_id} (#{@failed_job&.class_name})? [y/n]"
|
|
128
|
+
when :discard
|
|
129
|
+
"Discard job ##{@failed_job&.job_id} (#{@failed_job&.class_name})? This cannot be undone. [y/n]"
|
|
81
130
|
end
|
|
82
|
-
bindings
|
|
83
131
|
end
|
|
84
132
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
133
|
+
def execute_confirm_action(action)
|
|
134
|
+
case action
|
|
135
|
+
when :retry
|
|
136
|
+
Actions::RetryJob.call(@failed_job.id)
|
|
137
|
+
hide
|
|
138
|
+
:refresh
|
|
139
|
+
when :discard
|
|
140
|
+
Actions::DiscardJob.call(@failed_job.id)
|
|
141
|
+
hide
|
|
142
|
+
:refresh
|
|
92
143
|
end
|
|
93
144
|
end
|
|
94
145
|
|
|
95
|
-
private
|
|
96
|
-
|
|
97
146
|
def render_failed_detail(frame, area)
|
|
98
147
|
lines = []
|
|
99
148
|
|
|
@@ -202,6 +251,104 @@ module SolidQueueTui
|
|
|
202
251
|
)
|
|
203
252
|
end
|
|
204
253
|
|
|
254
|
+
def render_process_detail(frame, area)
|
|
255
|
+
lines = []
|
|
256
|
+
|
|
257
|
+
# Section 1: Process Info
|
|
258
|
+
lines << section_header("Process Info")
|
|
259
|
+
lines << detail_line("ID", @process.id.to_s)
|
|
260
|
+
lines << detail_line("Kind", @process.kind)
|
|
261
|
+
lines << detail_line("PID", @process.pid.to_s)
|
|
262
|
+
lines << detail_line("Hostname", @process.hostname || "n/a")
|
|
263
|
+
lines << detail_line("Name", @process.name || "n/a")
|
|
264
|
+
lines << empty_line
|
|
265
|
+
|
|
266
|
+
# Section 2: Status
|
|
267
|
+
lines << section_header("Status")
|
|
268
|
+
alive = @process.alive?
|
|
269
|
+
lines << @tui.text_line(spans: [
|
|
270
|
+
@tui.text_span(content: " #{"Status".ljust(16)}", style: @tui.style(fg: :dark_gray)),
|
|
271
|
+
@tui.text_span(content: alive ? "alive" : "dead",
|
|
272
|
+
style: @tui.style(fg: alive ? :green : :red, modifiers: [:bold]))
|
|
273
|
+
])
|
|
274
|
+
lines << detail_line("Last Heartbeat", time_ago(@process.last_heartbeat_at))
|
|
275
|
+
lines << detail_line("Uptime", format_duration(@process.uptime))
|
|
276
|
+
lines << detail_line("Created At", format_time(@process.created_at))
|
|
277
|
+
lines << empty_line
|
|
278
|
+
|
|
279
|
+
# Section 3: Configuration
|
|
280
|
+
lines << section_header("Configuration")
|
|
281
|
+
queues_str = Array(@process.queues).join(", ")
|
|
282
|
+
lines << detail_line("Queues", queues_str.empty? ? "n/a" : queues_str)
|
|
283
|
+
lines << detail_line("Threads", (@process.thread_count || "n/a").to_s)
|
|
284
|
+
if @process.metadata.is_a?(Hash) && @process.metadata["polling_interval"]
|
|
285
|
+
lines << detail_line("Poll Interval", "#{@process.metadata["polling_interval"]}s")
|
|
286
|
+
end
|
|
287
|
+
lines << detail_line("Supervisor ID", (@process.supervisor_id || "n/a").to_s)
|
|
288
|
+
lines << empty_line
|
|
289
|
+
|
|
290
|
+
# Section 4: Running Jobs (Workers only)
|
|
291
|
+
if @process.kind == "Worker"
|
|
292
|
+
lines << section_header("Running Jobs (#{@running_jobs.size})")
|
|
293
|
+
if @running_jobs.empty?
|
|
294
|
+
lines << @tui.text_line(spans: [
|
|
295
|
+
@tui.text_span(content: " Idle — no running jobs", style: @tui.style(fg: :dark_gray))
|
|
296
|
+
])
|
|
297
|
+
else
|
|
298
|
+
lines << @tui.text_line(spans: [
|
|
299
|
+
@tui.text_span(
|
|
300
|
+
content: " #{"ID".ljust(8)}#{"CLASS".ljust(30)}#{"QUEUE".ljust(16)}STARTED",
|
|
301
|
+
style: @tui.style(fg: :cyan, modifiers: [:bold])
|
|
302
|
+
)
|
|
303
|
+
])
|
|
304
|
+
@running_jobs.each do |rj|
|
|
305
|
+
lines << @tui.text_line(spans: [
|
|
306
|
+
@tui.text_span(content: " #{rj.job_id.to_s.ljust(8)}", style: @tui.style(fg: :white)),
|
|
307
|
+
@tui.text_span(content: rj.class_name.to_s.ljust(30), style: @tui.style(fg: :yellow)),
|
|
308
|
+
@tui.text_span(content: rj.queue_name.to_s.ljust(16), style: @tui.style(fg: :white)),
|
|
309
|
+
@tui.text_span(content: time_ago(rj.started_at), style: @tui.style(fg: :dark_gray))
|
|
310
|
+
])
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
lines << empty_line
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Section 5: Raw Metadata
|
|
317
|
+
if @process.metadata.is_a?(Hash) && !@process.metadata.empty?
|
|
318
|
+
lines << section_header("Raw Metadata")
|
|
319
|
+
meta_str = JSON.pretty_generate(@process.metadata) rescue @process.metadata.to_s
|
|
320
|
+
meta_str.split("\n").each do |meta_line|
|
|
321
|
+
lines << @tui.text_line(spans: [
|
|
322
|
+
@tui.text_span(content: " #{meta_line}", style: @tui.style(fg: :white))
|
|
323
|
+
])
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
visible_lines = lines.drop(@scroll_offset)
|
|
328
|
+
|
|
329
|
+
border_color = @process.alive? ? :green : :red
|
|
330
|
+
title_text = " #{@process.kind} ##{@process.id} — PID: #{@process.pid} "
|
|
331
|
+
|
|
332
|
+
frame.render_widget(
|
|
333
|
+
@tui.paragraph(
|
|
334
|
+
text: visible_lines,
|
|
335
|
+
block: @tui.block(
|
|
336
|
+
title: title_text,
|
|
337
|
+
title_style: @tui.style(fg: border_color, modifiers: [:bold]),
|
|
338
|
+
titles: [
|
|
339
|
+
{ content: " Esc:Close j/k:Scroll ",
|
|
340
|
+
position: :bottom, alignment: :right }
|
|
341
|
+
],
|
|
342
|
+
borders: [:all],
|
|
343
|
+
border_type: :rounded,
|
|
344
|
+
border_style: @tui.style(fg: border_color),
|
|
345
|
+
style: @tui.style(fg: :white)
|
|
346
|
+
)
|
|
347
|
+
),
|
|
348
|
+
area
|
|
349
|
+
)
|
|
350
|
+
end
|
|
351
|
+
|
|
205
352
|
def section_header(title)
|
|
206
353
|
@tui.text_line(spans: [
|
|
207
354
|
@tui.text_span(content: " ── #{title} ", style: @tui.style(fg: :cyan, modifiers: [:bold]))
|
|
@@ -221,6 +368,7 @@ module SolidQueueTui
|
|
|
221
368
|
])
|
|
222
369
|
end
|
|
223
370
|
|
|
371
|
+
# Override: includes UTC suffix for detail view precision
|
|
224
372
|
def format_time(time)
|
|
225
373
|
return "n/a" unless time
|
|
226
374
|
time.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class ProcessesView
|
|
6
|
+
include FormattingHelpers
|
|
7
|
+
|
|
6
8
|
KIND_COLORS = {
|
|
7
9
|
"Worker" => :green,
|
|
8
10
|
"Dispatcher" => :yellow,
|
|
@@ -113,30 +115,6 @@ module SolidQueueTui
|
|
|
113
115
|
@table_state.select(@selected_row)
|
|
114
116
|
end
|
|
115
117
|
|
|
116
|
-
def time_ago(time)
|
|
117
|
-
return "n/a" unless time
|
|
118
|
-
seconds = (Time.now.utc - time).to_i
|
|
119
|
-
case seconds
|
|
120
|
-
when 0..59 then "#{seconds}s ago"
|
|
121
|
-
when 60..3599 then "#{seconds / 60}m ago"
|
|
122
|
-
when 3600..86399 then "#{seconds / 3600}h ago"
|
|
123
|
-
else "#{seconds / 86400}d ago"
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def format_duration(seconds)
|
|
128
|
-
return "n/a" unless seconds
|
|
129
|
-
seconds = seconds.to_i
|
|
130
|
-
if seconds < 60
|
|
131
|
-
"#{seconds}s"
|
|
132
|
-
elsif seconds < 3600
|
|
133
|
-
"#{seconds / 60}m #{seconds % 60}s"
|
|
134
|
-
elsif seconds < 86400
|
|
135
|
-
"#{seconds / 3600}h #{(seconds % 3600) / 60}m"
|
|
136
|
-
else
|
|
137
|
-
"#{seconds / 86400}d #{(seconds % 86400) / 3600}h"
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
118
|
end
|
|
141
119
|
end
|
|
142
120
|
end
|