solid_queue_tui 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/exe/sqtui +6 -0
- data/lib/solid_queue_tui/actions/discard_job.rb +34 -0
- data/lib/solid_queue_tui/actions/discard_scheduled_job.rb +33 -0
- data/lib/solid_queue_tui/actions/dispatch_scheduled_job.rb +35 -0
- data/lib/solid_queue_tui/actions/retry_job.rb +76 -0
- data/lib/solid_queue_tui/application.rb +468 -0
- data/lib/solid_queue_tui/cli.rb +48 -0
- data/lib/solid_queue_tui/components/header.rb +105 -0
- data/lib/solid_queue_tui/components/help_bar.rb +77 -0
- data/lib/solid_queue_tui/components/job_table.rb +122 -0
- data/lib/solid_queue_tui/connection.rb +58 -0
- data/lib/solid_queue_tui/data/failed_query.rb +118 -0
- data/lib/solid_queue_tui/data/jobs_query.rb +178 -0
- data/lib/solid_queue_tui/data/processes_query.rb +75 -0
- data/lib/solid_queue_tui/data/queues_query.rb +36 -0
- data/lib/solid_queue_tui/data/stats.rb +65 -0
- data/lib/solid_queue_tui/dev_reloader.rb +53 -0
- data/lib/solid_queue_tui/version.rb +5 -0
- data/lib/solid_queue_tui/views/blocked_view.rb +121 -0
- data/lib/solid_queue_tui/views/dashboard_view.rb +187 -0
- data/lib/solid_queue_tui/views/failed_view.rb +298 -0
- data/lib/solid_queue_tui/views/finished_view.rb +216 -0
- data/lib/solid_queue_tui/views/in_progress_view.rb +114 -0
- data/lib/solid_queue_tui/views/job_detail_view.rb +236 -0
- data/lib/solid_queue_tui/views/processes_view.rb +142 -0
- data/lib/solid_queue_tui/views/queues_view.rb +96 -0
- data/lib/solid_queue_tui/views/scheduled_view.rb +215 -0
- data/lib/solid_queue_tui.rb +46 -0
- metadata +157 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
class InProgressView
|
|
6
|
+
def initialize(tui)
|
|
7
|
+
@tui = tui
|
|
8
|
+
@table_state = RatatuiRuby::TableState.new(nil)
|
|
9
|
+
@table_state.select(0)
|
|
10
|
+
@selected_row = 0
|
|
11
|
+
@jobs = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def update(jobs:)
|
|
15
|
+
@jobs = jobs
|
|
16
|
+
@selected_row = @selected_row.clamp(0, [@jobs.size - 1, 0].max)
|
|
17
|
+
@table_state.select(@selected_row)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(frame, area)
|
|
21
|
+
columns = [
|
|
22
|
+
{ key: :id, label: "ID", width: 8 },
|
|
23
|
+
{ key: :queue_name, label: "QUEUE", width: 14 },
|
|
24
|
+
{ key: :class_name, label: "CLASS", width: :fill },
|
|
25
|
+
{ key: :priority, label: "PRI", width: 5 },
|
|
26
|
+
{ key: :worker_id, label: "WORKER", width: 8 },
|
|
27
|
+
{ key: :started_at, label: "STARTED", width: 12 }
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
rows = @jobs.map do |job|
|
|
31
|
+
{
|
|
32
|
+
id: job.id,
|
|
33
|
+
queue_name: job.queue_name,
|
|
34
|
+
class_name: job.class_name,
|
|
35
|
+
priority: job.priority,
|
|
36
|
+
worker_id: job.worker_id || "n/a",
|
|
37
|
+
started_at: time_ago(job.started_at)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
table = Components::JobTable.new(
|
|
42
|
+
@tui,
|
|
43
|
+
title: "In Progress",
|
|
44
|
+
columns: columns,
|
|
45
|
+
rows: rows,
|
|
46
|
+
selected_row: @selected_row,
|
|
47
|
+
empty_message: "No jobs currently in progress"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
table.render(frame, area, @table_state)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_input(event)
|
|
54
|
+
case event
|
|
55
|
+
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
56
|
+
move_selection(-1)
|
|
57
|
+
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
58
|
+
move_selection(1)
|
|
59
|
+
in { type: :key, code: "g" }
|
|
60
|
+
jump_to_top
|
|
61
|
+
in { type: :key, code: "G" }
|
|
62
|
+
jump_to_bottom
|
|
63
|
+
else
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def selected_item
|
|
69
|
+
return nil if @jobs.empty? || @selected_row >= @jobs.size
|
|
70
|
+
@jobs[@selected_row]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def bindings
|
|
74
|
+
[
|
|
75
|
+
{ key: "j/k", action: "Navigate" },
|
|
76
|
+
{ key: "Enter", action: "Detail" },
|
|
77
|
+
{ key: "G/g", action: "Bottom/Top" }
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def breadcrumb = "in-progress"
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def move_selection(delta)
|
|
86
|
+
return if @jobs.empty?
|
|
87
|
+
@selected_row = (@selected_row + delta).clamp(0, @jobs.size - 1)
|
|
88
|
+
@table_state.select(@selected_row)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def jump_to_top
|
|
92
|
+
@selected_row = 0
|
|
93
|
+
@table_state.select(0)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def jump_to_bottom
|
|
97
|
+
return if @jobs.empty?
|
|
98
|
+
@selected_row = @jobs.size - 1
|
|
99
|
+
@table_state.select(@selected_row)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def time_ago(time)
|
|
103
|
+
return "n/a" unless time
|
|
104
|
+
seconds = (Time.now.utc - time).to_i
|
|
105
|
+
case seconds
|
|
106
|
+
when 0..59 then "#{seconds}s ago"
|
|
107
|
+
when 60..3599 then "#{seconds / 60}m ago"
|
|
108
|
+
when 3600..86399 then "#{seconds / 3600}h ago"
|
|
109
|
+
else "#{seconds / 86400}d ago"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
class JobDetailView
|
|
6
|
+
def initialize(tui)
|
|
7
|
+
@tui = tui
|
|
8
|
+
@job = nil
|
|
9
|
+
@failed_job = nil
|
|
10
|
+
@scroll_offset = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show(job: nil, failed_job: nil)
|
|
14
|
+
@job = job
|
|
15
|
+
@failed_job = failed_job
|
|
16
|
+
@scroll_offset = 0
|
|
17
|
+
@active = true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def hide
|
|
21
|
+
@active = false
|
|
22
|
+
@job = nil
|
|
23
|
+
@failed_job = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def active? = @active
|
|
27
|
+
|
|
28
|
+
def render(frame, area)
|
|
29
|
+
return unless @active
|
|
30
|
+
|
|
31
|
+
# Render an overlay with padding
|
|
32
|
+
inner = shrink_area(area, 4, 2)
|
|
33
|
+
|
|
34
|
+
frame.render_widget(@tui.paragraph(text: ""), inner) # clear background
|
|
35
|
+
|
|
36
|
+
if @failed_job
|
|
37
|
+
render_failed_detail(frame, inner)
|
|
38
|
+
elsif @job
|
|
39
|
+
render_job_detail(frame, inner)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def handle_input(event)
|
|
44
|
+
case event
|
|
45
|
+
in { type: :key, code: "esc" } | { type: :key, code: "q" }
|
|
46
|
+
hide
|
|
47
|
+
nil
|
|
48
|
+
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
49
|
+
@scroll_offset = [@scroll_offset - 1, 0].max
|
|
50
|
+
nil
|
|
51
|
+
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
52
|
+
@scroll_offset += 1
|
|
53
|
+
nil
|
|
54
|
+
in { type: :key, code: "R" }
|
|
55
|
+
if @failed_job
|
|
56
|
+
Actions::RetryJob.call(@failed_job.id)
|
|
57
|
+
hide
|
|
58
|
+
:refresh
|
|
59
|
+
end
|
|
60
|
+
in { type: :key, code: "D" }
|
|
61
|
+
if @failed_job
|
|
62
|
+
Actions::DiscardJob.call(@failed_job.id)
|
|
63
|
+
hide
|
|
64
|
+
:refresh
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def bindings
|
|
72
|
+
bindings = [
|
|
73
|
+
{ key: "Esc", action: "Close" },
|
|
74
|
+
{ key: "j/k", action: "Scroll" }
|
|
75
|
+
]
|
|
76
|
+
if @failed_job
|
|
77
|
+
bindings += [
|
|
78
|
+
{ key: "R", action: "Retry" },
|
|
79
|
+
{ key: "D", action: "Discard" }
|
|
80
|
+
]
|
|
81
|
+
end
|
|
82
|
+
bindings
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def breadcrumb
|
|
86
|
+
if @failed_job
|
|
87
|
+
"failed:#{@failed_job.job_id}"
|
|
88
|
+
elsif @job
|
|
89
|
+
"jobs:#{@job.id}"
|
|
90
|
+
else
|
|
91
|
+
"detail"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def render_failed_detail(frame, area)
|
|
98
|
+
lines = []
|
|
99
|
+
|
|
100
|
+
lines << section_header("Job Information")
|
|
101
|
+
lines << detail_line("Job ID", @failed_job.job_id.to_s)
|
|
102
|
+
lines << detail_line("Active Job ID", @failed_job.active_job_id || "n/a")
|
|
103
|
+
lines << detail_line("Class", @failed_job.class_name)
|
|
104
|
+
lines << detail_line("Queue", @failed_job.queue_name)
|
|
105
|
+
lines << detail_line("Priority", @failed_job.priority.to_s)
|
|
106
|
+
lines << detail_line("Created At", format_time(@failed_job.created_at))
|
|
107
|
+
lines << detail_line("Failed At", format_time(@failed_job.failed_at))
|
|
108
|
+
lines << empty_line
|
|
109
|
+
|
|
110
|
+
lines << section_header("Error Details")
|
|
111
|
+
lines << detail_line("Exception", @failed_job.error_class)
|
|
112
|
+
lines << detail_line("Message", @failed_job.error_message)
|
|
113
|
+
lines << empty_line
|
|
114
|
+
|
|
115
|
+
if @failed_job.backtrace.is_a?(Array) && !@failed_job.backtrace.empty?
|
|
116
|
+
lines << section_header("Backtrace")
|
|
117
|
+
@failed_job.backtrace.first(30).each do |bt_line|
|
|
118
|
+
lines << @tui.text_line(spans: [
|
|
119
|
+
@tui.text_span(content: " #{bt_line}", style: @tui.style(fg: :dark_gray))
|
|
120
|
+
])
|
|
121
|
+
end
|
|
122
|
+
lines << empty_line
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if @failed_job.arguments.is_a?(Hash) || @failed_job.arguments.is_a?(Array)
|
|
126
|
+
lines << section_header("Arguments")
|
|
127
|
+
args_str = JSON.pretty_generate(@failed_job.arguments) rescue @failed_job.arguments.to_s
|
|
128
|
+
args_str.split("\n").each do |arg_line|
|
|
129
|
+
lines << @tui.text_line(spans: [
|
|
130
|
+
@tui.text_span(content: " #{arg_line}", style: @tui.style(fg: :white))
|
|
131
|
+
])
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Apply scroll offset
|
|
136
|
+
visible_lines = lines.drop(@scroll_offset)
|
|
137
|
+
|
|
138
|
+
frame.render_widget(
|
|
139
|
+
@tui.paragraph(
|
|
140
|
+
text: visible_lines,
|
|
141
|
+
block: @tui.block(
|
|
142
|
+
title: " Failed Job ##{@failed_job.job_id} — #{@failed_job.class_name} ",
|
|
143
|
+
title_style: @tui.style(fg: :red, modifiers: [:bold]),
|
|
144
|
+
titles: [
|
|
145
|
+
{ content: " Esc:Close R:Retry D:Discard ",
|
|
146
|
+
position: :bottom, alignment: :right }
|
|
147
|
+
],
|
|
148
|
+
borders: [:all],
|
|
149
|
+
border_type: :rounded,
|
|
150
|
+
border_style: @tui.style(fg: :red),
|
|
151
|
+
style: @tui.style(fg: :white)
|
|
152
|
+
)
|
|
153
|
+
),
|
|
154
|
+
area
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def render_job_detail(frame, area)
|
|
159
|
+
lines = []
|
|
160
|
+
|
|
161
|
+
lines << section_header("Job Information")
|
|
162
|
+
lines << detail_line("ID", @job.id.to_s)
|
|
163
|
+
lines << detail_line("Active Job ID", @job.active_job_id || "n/a")
|
|
164
|
+
lines << detail_line("Class", @job.class_name)
|
|
165
|
+
lines << detail_line("Queue", @job.queue_name)
|
|
166
|
+
lines << detail_line("Priority", @job.priority.to_s)
|
|
167
|
+
lines << detail_line("Status", @job.status)
|
|
168
|
+
lines << detail_line("Created At", format_time(@job.created_at))
|
|
169
|
+
lines << detail_line("Scheduled At", format_time(@job.scheduled_at))
|
|
170
|
+
lines << detail_line("Finished At", format_time(@job.finished_at))
|
|
171
|
+
|
|
172
|
+
if @job.arguments.is_a?(Hash) || @job.arguments.is_a?(Array)
|
|
173
|
+
lines << empty_line
|
|
174
|
+
lines << section_header("Arguments")
|
|
175
|
+
args_str = JSON.pretty_generate(@job.arguments) rescue @job.arguments.to_s
|
|
176
|
+
args_str.split("\n").each do |arg_line|
|
|
177
|
+
lines << @tui.text_line(spans: [
|
|
178
|
+
@tui.text_span(content: " #{arg_line}", style: @tui.style(fg: :white))
|
|
179
|
+
])
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
visible_lines = lines.drop(@scroll_offset)
|
|
184
|
+
|
|
185
|
+
frame.render_widget(
|
|
186
|
+
@tui.paragraph(
|
|
187
|
+
text: visible_lines,
|
|
188
|
+
block: @tui.block(
|
|
189
|
+
title: " Job ##{@job.id} — #{@job.class_name} ",
|
|
190
|
+
title_style: @tui.style(fg: :cyan, modifiers: [:bold]),
|
|
191
|
+
titles: [
|
|
192
|
+
{ content: " Esc:Close ",
|
|
193
|
+
position: :bottom, alignment: :right }
|
|
194
|
+
],
|
|
195
|
+
borders: [:all],
|
|
196
|
+
border_type: :rounded,
|
|
197
|
+
border_style: @tui.style(fg: :cyan),
|
|
198
|
+
style: @tui.style(fg: :white)
|
|
199
|
+
)
|
|
200
|
+
),
|
|
201
|
+
area
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def section_header(title)
|
|
206
|
+
@tui.text_line(spans: [
|
|
207
|
+
@tui.text_span(content: " ── #{title} ", style: @tui.style(fg: :cyan, modifiers: [:bold]))
|
|
208
|
+
])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def detail_line(label, value)
|
|
212
|
+
@tui.text_line(spans: [
|
|
213
|
+
@tui.text_span(content: " #{label.ljust(16)}", style: @tui.style(fg: :dark_gray)),
|
|
214
|
+
@tui.text_span(content: value.to_s, style: @tui.style(fg: :white))
|
|
215
|
+
])
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def empty_line
|
|
219
|
+
@tui.text_line(spans: [
|
|
220
|
+
@tui.text_span(content: "", style: @tui.style(fg: :white))
|
|
221
|
+
])
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def format_time(time)
|
|
225
|
+
return "n/a" unless time
|
|
226
|
+
time.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def shrink_area(area, h_pad, v_pad)
|
|
230
|
+
# Create a smaller area centered in the given area
|
|
231
|
+
# This is an approximation - the actual implementation depends on ratatui_ruby's Rect API
|
|
232
|
+
area
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
class ProcessesView
|
|
6
|
+
KIND_COLORS = {
|
|
7
|
+
"Worker" => :green,
|
|
8
|
+
"Dispatcher" => :yellow,
|
|
9
|
+
"Scheduler" => :blue
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
def initialize(tui)
|
|
13
|
+
@tui = tui
|
|
14
|
+
@table_state = RatatuiRuby::TableState.new(nil)
|
|
15
|
+
@table_state.select(0)
|
|
16
|
+
@selected_row = 0
|
|
17
|
+
@processes = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def update(processes:)
|
|
21
|
+
@processes = processes
|
|
22
|
+
@selected_row = @selected_row.clamp(0, [@processes.size - 1, 0].max)
|
|
23
|
+
@table_state.select(@selected_row)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render(frame, area)
|
|
27
|
+
columns = [
|
|
28
|
+
{ key: :id, label: "ID", width: 6 },
|
|
29
|
+
{ key: :kind, label: "KIND", width: 12 },
|
|
30
|
+
{ key: :hostname, label: "HOSTNAME", width: :fill },
|
|
31
|
+
{ key: :pid, label: "PID", width: 8 },
|
|
32
|
+
{ key: :name, label: "NAME", width: :fill },
|
|
33
|
+
{ key: :queues, label: "QUEUES", width: :fill },
|
|
34
|
+
{ key: :heartbeat, label: "HEARTBEAT", width: 12 },
|
|
35
|
+
{ key: :uptime, label: "UPTIME", width: 10 },
|
|
36
|
+
{ key: :status, label: "STATUS", width: 8 }
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
rows = @processes.map do |proc|
|
|
40
|
+
alive = proc.alive?
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
id: proc.id,
|
|
44
|
+
kind: proc.kind,
|
|
45
|
+
hostname: proc.hostname || "n/a",
|
|
46
|
+
pid: proc.pid,
|
|
47
|
+
name: proc.name || "n/a",
|
|
48
|
+
queues: Array(proc.queues).join(", "),
|
|
49
|
+
heartbeat: time_ago(proc.last_heartbeat_at),
|
|
50
|
+
uptime: format_duration(proc.uptime),
|
|
51
|
+
status: alive ? "alive" : "dead"
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
table = Components::JobTable.new(
|
|
56
|
+
@tui,
|
|
57
|
+
title: "Processes",
|
|
58
|
+
columns: columns,
|
|
59
|
+
rows: rows,
|
|
60
|
+
selected_row: @selected_row,
|
|
61
|
+
empty_message: "No active processes — is Solid Queue running?"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
table.render(frame, area, @table_state)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def handle_input(event)
|
|
68
|
+
case event
|
|
69
|
+
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
70
|
+
move_selection(-1)
|
|
71
|
+
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
72
|
+
move_selection(1)
|
|
73
|
+
in { type: :key, code: "g" }
|
|
74
|
+
jump_to_top
|
|
75
|
+
in { type: :key, code: "G" }
|
|
76
|
+
jump_to_bottom
|
|
77
|
+
else
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def selected_item
|
|
83
|
+
return nil if @processes.empty? || @selected_row >= @processes.size
|
|
84
|
+
@processes[@selected_row]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def bindings
|
|
88
|
+
[
|
|
89
|
+
{ key: "j/k", action: "Navigate" },
|
|
90
|
+
{ key: "Enter", action: "Detail" },
|
|
91
|
+
{ key: "G/g", action: "Bottom/Top" }
|
|
92
|
+
]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def breadcrumb = "processes"
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def move_selection(delta)
|
|
100
|
+
return if @processes.empty?
|
|
101
|
+
@selected_row = (@selected_row + delta).clamp(0, @processes.size - 1)
|
|
102
|
+
@table_state.select(@selected_row)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def jump_to_top
|
|
106
|
+
@selected_row = 0
|
|
107
|
+
@table_state.select(0)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def jump_to_bottom
|
|
111
|
+
return if @processes.empty?
|
|
112
|
+
@selected_row = @processes.size - 1
|
|
113
|
+
@table_state.select(@selected_row)
|
|
114
|
+
end
|
|
115
|
+
|
|
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
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
class QueuesView
|
|
6
|
+
def initialize(tui)
|
|
7
|
+
@tui = tui
|
|
8
|
+
@table_state = RatatuiRuby::TableState.new(nil)
|
|
9
|
+
@table_state.select(0)
|
|
10
|
+
@selected_row = 0
|
|
11
|
+
@queues = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def update(queues:)
|
|
15
|
+
@queues = queues
|
|
16
|
+
@selected_row = @selected_row.clamp(0, [@queues.size - 1, 0].max)
|
|
17
|
+
@table_state.select(@selected_row)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(frame, area)
|
|
21
|
+
columns = [
|
|
22
|
+
{ key: :name, label: "QUEUE", width: :fill },
|
|
23
|
+
{ key: :size, label: "SIZE", width: 10 },
|
|
24
|
+
{ key: :status, label: "STATUS", width: 10, color_by: :status }
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
rows = @queues.map do |q|
|
|
28
|
+
{
|
|
29
|
+
name: q.name,
|
|
30
|
+
size: q.size,
|
|
31
|
+
status: q.paused ? "paused" : "active"
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
table = Components::JobTable.new(
|
|
36
|
+
@tui,
|
|
37
|
+
title: "Queues",
|
|
38
|
+
columns: columns,
|
|
39
|
+
rows: rows,
|
|
40
|
+
selected_row: @selected_row,
|
|
41
|
+
empty_message: "No queues found"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
table.render(frame, area, @table_state)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_input(event)
|
|
48
|
+
case event
|
|
49
|
+
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
50
|
+
move_selection(-1)
|
|
51
|
+
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
52
|
+
move_selection(1)
|
|
53
|
+
in { type: :key, code: "g" }
|
|
54
|
+
jump_to_top
|
|
55
|
+
in { type: :key, code: "G" }
|
|
56
|
+
jump_to_bottom
|
|
57
|
+
else
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def selected_item
|
|
63
|
+
return nil if @queues.empty? || @selected_row >= @queues.size
|
|
64
|
+
@queues[@selected_row]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def bindings
|
|
68
|
+
[
|
|
69
|
+
{ key: "j/k", action: "Navigate" },
|
|
70
|
+
{ key: "G/g", action: "Bottom/Top" }
|
|
71
|
+
]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def breadcrumb = "queues"
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def move_selection(delta)
|
|
79
|
+
return if @queues.empty?
|
|
80
|
+
@selected_row = (@selected_row + delta).clamp(0, @queues.size - 1)
|
|
81
|
+
@table_state.select(@selected_row)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def jump_to_top
|
|
85
|
+
@selected_row = 0
|
|
86
|
+
@table_state.select(0)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def jump_to_bottom
|
|
90
|
+
return if @queues.empty?
|
|
91
|
+
@selected_row = @queues.size - 1
|
|
92
|
+
@table_state.select(@selected_row)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|