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