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
|
@@ -3,84 +3,141 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class QueuesView
|
|
6
|
+
include Confirmable
|
|
7
|
+
include Filterable
|
|
8
|
+
include Paginatable
|
|
9
|
+
include FormattingHelpers
|
|
10
|
+
|
|
6
11
|
def initialize(tui)
|
|
7
12
|
@tui = tui
|
|
8
|
-
@
|
|
9
|
-
@
|
|
10
|
-
|
|
13
|
+
@mode = :list
|
|
14
|
+
@selected_queue = nil
|
|
15
|
+
|
|
16
|
+
# List mode state
|
|
17
|
+
@list_table_state = RatatuiRuby::TableState.new(nil)
|
|
18
|
+
@list_table_state.select(0)
|
|
19
|
+
@list_selected_row = 0
|
|
11
20
|
@queues = []
|
|
12
|
-
|
|
21
|
+
|
|
22
|
+
# Detail mode state (via Paginatable + Filterable)
|
|
23
|
+
init_pagination
|
|
24
|
+
init_confirm
|
|
25
|
+
init_filter
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def detail_mode? = @mode == :detail
|
|
29
|
+
|
|
30
|
+
def selected_queue_name
|
|
31
|
+
@selected_queue&.name
|
|
13
32
|
end
|
|
14
33
|
|
|
15
34
|
def update(queues:)
|
|
16
35
|
@queues = queues
|
|
17
|
-
@
|
|
18
|
-
@
|
|
36
|
+
@list_selected_row = @list_selected_row.clamp(0, [@queues.size - 1, 0].max)
|
|
37
|
+
@list_table_state.select(@list_selected_row)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update_detail(jobs:)
|
|
41
|
+
update_items(jobs)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def append(jobs:)
|
|
45
|
+
append_items(jobs)
|
|
19
46
|
end
|
|
20
47
|
|
|
21
48
|
def render(frame, area)
|
|
22
|
-
if @
|
|
23
|
-
|
|
24
|
-
area,
|
|
25
|
-
direction: :vertical,
|
|
26
|
-
constraints: [
|
|
27
|
-
@tui.constraint_length(3),
|
|
28
|
-
@tui.constraint_fill(1)
|
|
29
|
-
]
|
|
30
|
-
)
|
|
31
|
-
render_confirm(frame, confirm_area)
|
|
32
|
-
render_table(frame, content_area)
|
|
49
|
+
if @mode == :list
|
|
50
|
+
render_list(frame, area)
|
|
33
51
|
else
|
|
34
|
-
|
|
52
|
+
render_detail(frame, area)
|
|
35
53
|
end
|
|
36
54
|
end
|
|
37
55
|
|
|
38
56
|
def handle_input(event)
|
|
39
|
-
if @
|
|
57
|
+
if @mode == :list
|
|
58
|
+
handle_list_input(event)
|
|
59
|
+
elsif confirm_mode?
|
|
40
60
|
handle_confirm_input(event)
|
|
61
|
+
elsif filter_mode?
|
|
62
|
+
handle_filter_input(event)
|
|
41
63
|
else
|
|
42
|
-
|
|
64
|
+
handle_detail_input(event)
|
|
43
65
|
end
|
|
44
66
|
end
|
|
45
67
|
|
|
46
68
|
def selected_item
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
if @mode == :list
|
|
70
|
+
return nil if @queues.empty? || @list_selected_row >= @queues.size
|
|
71
|
+
@queues[@list_selected_row]
|
|
72
|
+
else
|
|
73
|
+
return nil if items.empty? || @selected_row >= items.size
|
|
74
|
+
items[@selected_row]
|
|
75
|
+
end
|
|
49
76
|
end
|
|
50
77
|
|
|
51
78
|
def capturing_input?
|
|
52
|
-
|
|
79
|
+
detail_mode? || confirm_mode? || filter_mode?
|
|
53
80
|
end
|
|
54
81
|
|
|
55
82
|
def bindings
|
|
56
|
-
if @
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
if @mode == :list
|
|
84
|
+
if confirm_mode?
|
|
85
|
+
confirm_bindings
|
|
86
|
+
else
|
|
87
|
+
[
|
|
88
|
+
{ key: "j/k", action: "Navigate" },
|
|
89
|
+
{ key: "Enter", action: "View Jobs" },
|
|
90
|
+
{ key: "p", action: "Pause/Resume" },
|
|
91
|
+
{ key: "G/g", action: "Bottom/Top" }
|
|
92
|
+
]
|
|
93
|
+
end
|
|
61
94
|
else
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
if confirm_mode?
|
|
96
|
+
confirm_bindings
|
|
97
|
+
elsif filter_mode?
|
|
98
|
+
filter_bindings
|
|
99
|
+
else
|
|
100
|
+
[
|
|
101
|
+
{ key: "j/k", action: "Navigate" },
|
|
102
|
+
{ key: "Enter", action: "Detail" },
|
|
103
|
+
{ key: "/", action: "Filter" },
|
|
104
|
+
clear_filter_binding,
|
|
105
|
+
{ key: "Esc", action: "Back" },
|
|
106
|
+
{ key: "G/g", action: "Bottom/Top" }
|
|
107
|
+
].compact
|
|
108
|
+
end
|
|
67
109
|
end
|
|
68
110
|
end
|
|
69
111
|
|
|
70
|
-
def breadcrumb
|
|
112
|
+
def breadcrumb
|
|
113
|
+
if @mode == :list
|
|
114
|
+
"queues"
|
|
115
|
+
else
|
|
116
|
+
base = "queues > #{@selected_queue&.name}"
|
|
117
|
+
@filters.empty? ? base : "#{base}:filtered"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
71
120
|
|
|
72
121
|
private
|
|
73
122
|
|
|
74
|
-
|
|
123
|
+
# === List mode ===
|
|
124
|
+
|
|
125
|
+
def handle_list_input(event)
|
|
126
|
+
if confirm_mode?
|
|
127
|
+
return handle_confirm_input(event)
|
|
128
|
+
end
|
|
129
|
+
|
|
75
130
|
case event
|
|
76
131
|
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
77
|
-
|
|
132
|
+
list_move_selection(-1)
|
|
78
133
|
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
79
|
-
|
|
134
|
+
list_move_selection(1)
|
|
80
135
|
in { type: :key, code: "g" }
|
|
81
|
-
|
|
136
|
+
list_jump_to_top
|
|
82
137
|
in { type: :key, code: "G" }
|
|
83
|
-
|
|
138
|
+
list_jump_to_bottom
|
|
139
|
+
in { type: :key, code: "enter" }
|
|
140
|
+
enter_detail_mode
|
|
84
141
|
in { type: :key, code: "p" }
|
|
85
142
|
queue = selected_item
|
|
86
143
|
if queue
|
|
@@ -92,40 +149,46 @@ module SolidQueueTui
|
|
|
92
149
|
end
|
|
93
150
|
end
|
|
94
151
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
in { type: :key, code: "n" } | { type: :key, code: "esc" }
|
|
104
|
-
@confirm_action = nil
|
|
105
|
-
nil
|
|
106
|
-
else
|
|
107
|
-
nil
|
|
108
|
-
end
|
|
152
|
+
def enter_detail_mode
|
|
153
|
+
queue = selected_item
|
|
154
|
+
return nil unless queue
|
|
155
|
+
@selected_queue = queue
|
|
156
|
+
@mode = :detail
|
|
157
|
+
reset_pagination!
|
|
158
|
+
clear_filter
|
|
159
|
+
:enter_queue
|
|
109
160
|
end
|
|
110
161
|
|
|
111
|
-
def
|
|
162
|
+
def list_move_selection(delta)
|
|
112
163
|
return if @queues.empty?
|
|
113
|
-
@
|
|
114
|
-
@
|
|
164
|
+
@list_selected_row = (@list_selected_row + delta).clamp(0, @queues.size - 1)
|
|
165
|
+
@list_table_state.select(@list_selected_row)
|
|
166
|
+
nil
|
|
115
167
|
end
|
|
116
168
|
|
|
117
|
-
def
|
|
118
|
-
@
|
|
119
|
-
@
|
|
169
|
+
def list_jump_to_top
|
|
170
|
+
@list_selected_row = 0
|
|
171
|
+
@list_table_state.select(0)
|
|
172
|
+
nil
|
|
120
173
|
end
|
|
121
174
|
|
|
122
|
-
def
|
|
123
|
-
return if @queues.empty?
|
|
124
|
-
@
|
|
125
|
-
@
|
|
175
|
+
def list_jump_to_bottom
|
|
176
|
+
return nil if @queues.empty?
|
|
177
|
+
@list_selected_row = @queues.size - 1
|
|
178
|
+
@list_table_state.select(@list_selected_row)
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def render_list(frame, area)
|
|
183
|
+
if confirm_mode?
|
|
184
|
+
render_list_table(frame, area)
|
|
185
|
+
render_confirm_popup(frame, area)
|
|
186
|
+
else
|
|
187
|
+
render_list_table(frame, area)
|
|
188
|
+
end
|
|
126
189
|
end
|
|
127
190
|
|
|
128
|
-
def
|
|
191
|
+
def render_list_table(frame, area)
|
|
129
192
|
columns = [
|
|
130
193
|
{ key: :name, label: "QUEUE", width: :fill },
|
|
131
194
|
{ key: :size, label: "SIZE", width: 10 },
|
|
@@ -145,36 +208,112 @@ module SolidQueueTui
|
|
|
145
208
|
title: "Queues",
|
|
146
209
|
columns: columns,
|
|
147
210
|
rows: rows,
|
|
148
|
-
selected_row: @
|
|
211
|
+
selected_row: @list_selected_row,
|
|
149
212
|
empty_message: "No queues found"
|
|
150
213
|
)
|
|
151
214
|
|
|
152
|
-
table.render(frame, area, @
|
|
215
|
+
table.render(frame, area, @list_table_state)
|
|
153
216
|
end
|
|
154
217
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
218
|
+
# === Detail mode ===
|
|
219
|
+
|
|
220
|
+
def handle_detail_input(event)
|
|
221
|
+
case event
|
|
222
|
+
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
223
|
+
move_selection(-1)
|
|
224
|
+
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
225
|
+
result = move_selection(1)
|
|
226
|
+
return :load_more if result == :load_more
|
|
227
|
+
nil
|
|
228
|
+
in { type: :key, code: "g" }
|
|
229
|
+
jump_to_top
|
|
230
|
+
in { type: :key, code: "G" }
|
|
231
|
+
jump_to_bottom
|
|
232
|
+
in { type: :key, code: "enter" }
|
|
233
|
+
:open_detail
|
|
234
|
+
in { type: :key, code: "/" }
|
|
235
|
+
enter_filter_mode
|
|
236
|
+
nil
|
|
237
|
+
in { type: :key, code: "c" }
|
|
238
|
+
clear_filter
|
|
239
|
+
in { type: :key, code: "esc" }
|
|
240
|
+
exit_detail_mode
|
|
241
|
+
else
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def exit_detail_mode
|
|
247
|
+
@mode = :list
|
|
248
|
+
@selected_queue = nil
|
|
249
|
+
:exit_queue
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def render_detail(frame, area)
|
|
253
|
+
if filter_mode?
|
|
254
|
+
filter_area, content_area = @tui.layout_split(
|
|
255
|
+
area,
|
|
256
|
+
direction: :vertical,
|
|
257
|
+
constraints: [
|
|
258
|
+
@tui.constraint_length(3),
|
|
259
|
+
@tui.constraint_fill(1)
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
render_filter_input(frame, filter_area)
|
|
263
|
+
render_detail_table(frame, content_area)
|
|
264
|
+
else
|
|
265
|
+
render_detail_table(frame, area)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def render_detail_table(frame, area)
|
|
270
|
+
columns = [
|
|
271
|
+
{ key: :id, label: "ID", width: 8 },
|
|
272
|
+
{ key: :class_name, label: "CLASS", width: :fill },
|
|
273
|
+
{ key: :priority, label: "PRI", width: 5 },
|
|
274
|
+
{ key: :created_at, label: "ENQUEUED", width: 12 }
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
rows = items.map do |job|
|
|
278
|
+
{
|
|
279
|
+
id: job.id,
|
|
280
|
+
class_name: job.class_name,
|
|
281
|
+
priority: job.priority,
|
|
282
|
+
created_at: time_ago(job.created_at)
|
|
283
|
+
}
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
table = Components::JobTable.new(
|
|
287
|
+
@tui,
|
|
288
|
+
title: filter_title("Queue '#{@selected_queue&.name}' — Pending"),
|
|
289
|
+
columns: columns,
|
|
290
|
+
rows: rows,
|
|
291
|
+
selected_row: @selected_row,
|
|
292
|
+
total_count: @total_count,
|
|
293
|
+
empty_message: "No pending jobs in this queue"
|
|
176
294
|
)
|
|
295
|
+
|
|
296
|
+
table.render(frame, area, @table_state)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# === Confirmable hooks (list mode only) ===
|
|
300
|
+
|
|
301
|
+
def confirm_message
|
|
302
|
+
queue = @queues[@list_selected_row]
|
|
303
|
+
if @confirm_action == :pause
|
|
304
|
+
"Pause queue '#{queue&.name}'? Workers will stop picking up jobs from this queue. [y/n]"
|
|
305
|
+
else
|
|
306
|
+
"Resume queue '#{queue&.name}'? [y/n]"
|
|
307
|
+
end
|
|
177
308
|
end
|
|
309
|
+
|
|
310
|
+
def execute_confirm_action(action)
|
|
311
|
+
queue = @queues[@list_selected_row]
|
|
312
|
+
return nil unless queue
|
|
313
|
+
Actions::ToggleQueuePause.call(queue.name)
|
|
314
|
+
:refresh
|
|
315
|
+
end
|
|
316
|
+
|
|
178
317
|
end
|
|
179
318
|
end
|
|
180
319
|
end
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class RecurringTasksView
|
|
6
|
+
include Confirmable
|
|
7
|
+
include FormattingHelpers
|
|
8
|
+
|
|
6
9
|
def initialize(tui)
|
|
7
10
|
@tui = tui
|
|
8
11
|
@table_state = RatatuiRuby::TableState.new(nil)
|
|
9
12
|
@table_state.select(0)
|
|
10
13
|
@selected_row = 0
|
|
11
14
|
@tasks = []
|
|
12
|
-
|
|
15
|
+
init_confirm
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def update(tasks:)
|
|
@@ -19,24 +22,16 @@ module SolidQueueTui
|
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def render(frame, area)
|
|
22
|
-
if
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
direction: :vertical,
|
|
26
|
-
constraints: [
|
|
27
|
-
@tui.constraint_length(3),
|
|
28
|
-
@tui.constraint_fill(1)
|
|
29
|
-
]
|
|
30
|
-
)
|
|
31
|
-
render_confirm(frame, confirm_area)
|
|
32
|
-
render_table(frame, content_area)
|
|
25
|
+
if confirm_mode?
|
|
26
|
+
render_table(frame, area)
|
|
27
|
+
render_confirm_popup(frame, area)
|
|
33
28
|
else
|
|
34
29
|
render_table(frame, area)
|
|
35
30
|
end
|
|
36
31
|
end
|
|
37
32
|
|
|
38
33
|
def handle_input(event)
|
|
39
|
-
if
|
|
34
|
+
if confirm_mode?
|
|
40
35
|
handle_confirm_input(event)
|
|
41
36
|
else
|
|
42
37
|
handle_normal_input(event)
|
|
@@ -49,15 +44,12 @@ module SolidQueueTui
|
|
|
49
44
|
end
|
|
50
45
|
|
|
51
46
|
def capturing_input?
|
|
52
|
-
|
|
47
|
+
confirm_mode?
|
|
53
48
|
end
|
|
54
49
|
|
|
55
50
|
def bindings
|
|
56
|
-
if
|
|
57
|
-
|
|
58
|
-
{ key: "y", action: "Confirm" },
|
|
59
|
-
{ key: "n/Esc", action: "Cancel" }
|
|
60
|
-
]
|
|
51
|
+
if confirm_mode?
|
|
52
|
+
confirm_bindings
|
|
61
53
|
else
|
|
62
54
|
[
|
|
63
55
|
{ key: "j/k", action: "Navigate" },
|
|
@@ -89,20 +81,16 @@ module SolidQueueTui
|
|
|
89
81
|
end
|
|
90
82
|
end
|
|
91
83
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
nil
|
|
103
|
-
else
|
|
104
|
-
nil
|
|
105
|
-
end
|
|
84
|
+
def confirm_message
|
|
85
|
+
task = selected_item
|
|
86
|
+
"Run '#{task&.key}' (#{task&.class_name || task&.command}) now? [y/n]"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def execute_confirm_action(action)
|
|
90
|
+
task = selected_item
|
|
91
|
+
return nil unless task
|
|
92
|
+
Actions::EnqueueRecurringTask.call(task.key)
|
|
93
|
+
:refresh
|
|
106
94
|
end
|
|
107
95
|
|
|
108
96
|
def move_selection(delta)
|
|
@@ -155,48 +143,13 @@ module SolidQueueTui
|
|
|
155
143
|
table.render(frame, area, @table_state)
|
|
156
144
|
end
|
|
157
145
|
|
|
158
|
-
|
|
159
|
-
task = selected_item
|
|
160
|
-
message = "Run '#{task&.key}' (#{task&.class_name || task&.command}) now? [y/n]"
|
|
161
|
-
|
|
162
|
-
frame.render_widget(
|
|
163
|
-
@tui.paragraph(
|
|
164
|
-
text: " #{message}",
|
|
165
|
-
style: @tui.style(fg: :yellow, modifiers: [:bold]),
|
|
166
|
-
block: @tui.block(
|
|
167
|
-
title: " Confirm ",
|
|
168
|
-
title_style: @tui.style(fg: :red, modifiers: [:bold]),
|
|
169
|
-
borders: [:all],
|
|
170
|
-
border_type: :rounded,
|
|
171
|
-
border_style: @tui.style(fg: :red)
|
|
172
|
-
)
|
|
173
|
-
),
|
|
174
|
-
area
|
|
175
|
-
)
|
|
176
|
-
end
|
|
177
|
-
|
|
146
|
+
# Override: returns "Never" for nil and "just now" for very recent
|
|
178
147
|
def time_ago(time)
|
|
179
148
|
return "Never" unless time
|
|
180
149
|
seconds = (Time.now.utc - time).to_i
|
|
181
150
|
return "just now" if seconds < 5
|
|
182
151
|
"#{humanize_duration(seconds)} ago"
|
|
183
152
|
end
|
|
184
|
-
|
|
185
|
-
def time_until(time)
|
|
186
|
-
return "n/a" unless time
|
|
187
|
-
seconds = (time - Time.now.utc).to_i
|
|
188
|
-
return "now" if seconds <= 0
|
|
189
|
-
"in #{humanize_duration(seconds)}"
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def humanize_duration(seconds)
|
|
193
|
-
case seconds.abs
|
|
194
|
-
when 0..59 then "#{seconds.abs}s"
|
|
195
|
-
when 60..3599 then "#{seconds.abs / 60}m"
|
|
196
|
-
when 3600..86399 then "#{seconds.abs / 3600}h"
|
|
197
|
-
else "#{seconds.abs / 86400}d"
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
153
|
end
|
|
201
154
|
end
|
|
202
155
|
end
|