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,84 +3,140 @@
|
|
|
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
|
+
{ key: "Esc", action: "Back" },
|
|
105
|
+
{ key: "G/g", action: "Bottom/Top" }
|
|
106
|
+
]
|
|
107
|
+
end
|
|
67
108
|
end
|
|
68
109
|
end
|
|
69
110
|
|
|
70
|
-
def breadcrumb
|
|
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
|
|
71
119
|
|
|
72
120
|
private
|
|
73
121
|
|
|
74
|
-
|
|
122
|
+
# === List mode ===
|
|
123
|
+
|
|
124
|
+
def handle_list_input(event)
|
|
125
|
+
if confirm_mode?
|
|
126
|
+
return handle_confirm_input(event)
|
|
127
|
+
end
|
|
128
|
+
|
|
75
129
|
case event
|
|
76
130
|
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
77
|
-
|
|
131
|
+
list_move_selection(-1)
|
|
78
132
|
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
79
|
-
|
|
133
|
+
list_move_selection(1)
|
|
80
134
|
in { type: :key, code: "g" }
|
|
81
|
-
|
|
135
|
+
list_jump_to_top
|
|
82
136
|
in { type: :key, code: "G" }
|
|
83
|
-
|
|
137
|
+
list_jump_to_bottom
|
|
138
|
+
in { type: :key, code: "enter" }
|
|
139
|
+
enter_detail_mode
|
|
84
140
|
in { type: :key, code: "p" }
|
|
85
141
|
queue = selected_item
|
|
86
142
|
if queue
|
|
@@ -92,40 +148,46 @@ module SolidQueueTui
|
|
|
92
148
|
end
|
|
93
149
|
end
|
|
94
150
|
|
|
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
|
|
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
|
|
109
159
|
end
|
|
110
160
|
|
|
111
|
-
def
|
|
161
|
+
def list_move_selection(delta)
|
|
112
162
|
return if @queues.empty?
|
|
113
|
-
@
|
|
114
|
-
@
|
|
163
|
+
@list_selected_row = (@list_selected_row + delta).clamp(0, @queues.size - 1)
|
|
164
|
+
@list_table_state.select(@list_selected_row)
|
|
165
|
+
nil
|
|
115
166
|
end
|
|
116
167
|
|
|
117
|
-
def
|
|
118
|
-
@
|
|
119
|
-
@
|
|
168
|
+
def list_jump_to_top
|
|
169
|
+
@list_selected_row = 0
|
|
170
|
+
@list_table_state.select(0)
|
|
171
|
+
nil
|
|
120
172
|
end
|
|
121
173
|
|
|
122
|
-
def
|
|
123
|
-
return if @queues.empty?
|
|
124
|
-
@
|
|
125
|
-
@
|
|
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
|
|
126
188
|
end
|
|
127
189
|
|
|
128
|
-
def
|
|
190
|
+
def render_list_table(frame, area)
|
|
129
191
|
columns = [
|
|
130
192
|
{ key: :name, label: "QUEUE", width: :fill },
|
|
131
193
|
{ key: :size, label: "SIZE", width: 10 },
|
|
@@ -145,36 +207,110 @@ module SolidQueueTui
|
|
|
145
207
|
title: "Queues",
|
|
146
208
|
columns: columns,
|
|
147
209
|
rows: rows,
|
|
148
|
-
selected_row: @
|
|
210
|
+
selected_row: @list_selected_row,
|
|
149
211
|
empty_message: "No queues found"
|
|
150
212
|
)
|
|
151
213
|
|
|
152
|
-
table.render(frame, area, @
|
|
214
|
+
table.render(frame, area, @list_table_state)
|
|
153
215
|
end
|
|
154
216
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
217
|
+
# === Detail mode ===
|
|
218
|
+
|
|
219
|
+
def handle_detail_input(event)
|
|
220
|
+
case event
|
|
221
|
+
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
222
|
+
move_selection(-1)
|
|
223
|
+
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
224
|
+
result = move_selection(1)
|
|
225
|
+
return :load_more if result == :load_more
|
|
226
|
+
nil
|
|
227
|
+
in { type: :key, code: "g" }
|
|
228
|
+
jump_to_top
|
|
229
|
+
in { type: :key, code: "G" }
|
|
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
|
|
238
|
+
else
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def exit_detail_mode
|
|
244
|
+
@mode = :list
|
|
245
|
+
@selected_queue = nil
|
|
246
|
+
:exit_queue
|
|
247
|
+
end
|
|
248
|
+
|
|
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
|
|
264
|
+
end
|
|
265
|
+
|
|
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
|
+
]
|
|
273
|
+
|
|
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
|
|
282
|
+
|
|
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"
|
|
176
291
|
)
|
|
292
|
+
|
|
293
|
+
table.render(frame, area, @table_state)
|
|
294
|
+
end
|
|
295
|
+
|
|
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
|
|
177
305
|
end
|
|
306
|
+
|
|
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
|
|
312
|
+
end
|
|
313
|
+
|
|
178
314
|
end
|
|
179
315
|
end
|
|
180
316
|
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
|