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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/exe/sqtui +37 -1
  3. data/lib/solid_queue_tui/actions/discard_job.rb +4 -21
  4. data/lib/solid_queue_tui/actions/discard_scheduled_job.rb +5 -21
  5. data/lib/solid_queue_tui/actions/dispatch_scheduled_job.rb +4 -23
  6. data/lib/solid_queue_tui/actions/enqueue_recurring_task.rb +12 -0
  7. data/lib/solid_queue_tui/actions/retry_job.rb +14 -59
  8. data/lib/solid_queue_tui/actions/toggle_queue_pause.rb +19 -0
  9. data/lib/solid_queue_tui/application.rb +109 -21
  10. data/lib/solid_queue_tui/cli.rb +15 -8
  11. data/lib/solid_queue_tui/components/header.rb +26 -27
  12. data/lib/solid_queue_tui/components/help_bar.rb +1 -0
  13. data/lib/solid_queue_tui/components/job_table.rb +13 -2
  14. data/lib/solid_queue_tui/data/failed_query.rb +37 -91
  15. data/lib/solid_queue_tui/data/jobs_query.rb +119 -121
  16. data/lib/solid_queue_tui/data/processes_query.rb +32 -33
  17. data/lib/solid_queue_tui/data/queues_query.rb +6 -15
  18. data/lib/solid_queue_tui/data/recurring_tasks_query.rb +36 -0
  19. data/lib/solid_queue_tui/data/stats.rb +9 -27
  20. data/lib/solid_queue_tui/formatting_helpers.rb +63 -0
  21. data/lib/solid_queue_tui/railtie.rb +9 -0
  22. data/lib/solid_queue_tui/version.rb +1 -1
  23. data/lib/solid_queue_tui/views/blocked_view.rb +85 -74
  24. data/lib/solid_queue_tui/views/concerns/confirmable.rb +53 -0
  25. data/lib/solid_queue_tui/views/concerns/filterable.rb +128 -0
  26. data/lib/solid_queue_tui/views/concerns/paginatable.rb +79 -0
  27. data/lib/solid_queue_tui/views/dashboard_view.rb +4 -5
  28. data/lib/solid_queue_tui/views/failed_view.rb +65 -179
  29. data/lib/solid_queue_tui/views/finished_view.rb +33 -114
  30. data/lib/solid_queue_tui/views/in_progress_view.rb +85 -69
  31. data/lib/solid_queue_tui/views/job_detail_view.rb +179 -31
  32. data/lib/solid_queue_tui/views/processes_view.rb +2 -24
  33. data/lib/solid_queue_tui/views/queues_view.rb +250 -30
  34. data/lib/solid_queue_tui/views/recurring_tasks_view.rb +155 -0
  35. data/lib/solid_queue_tui/views/scheduled_view.rb +69 -107
  36. data/lib/solid_queue_tui.rb +18 -4
  37. data/lib/tasks/solid_queue_tui.rake +8 -0
  38. metadata +20 -25
  39. 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
- @table_state = RatatuiRuby::TableState.new(nil)
9
- @table_state.select(0)
10
- @selected_row = 0
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
- @selected_row = @selected_row.clamp(0, [@queues.size - 1, 0].max)
17
- @table_state.select(@selected_row)
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: @selected_row,
210
+ selected_row: @list_selected_row,
41
211
  empty_message: "No queues found"
42
212
  )
43
213
 
44
- table.render(frame, area, @table_state)
214
+ table.render(frame, area, @list_table_state)
45
215
  end
46
216
 
47
- def handle_input(event)
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 selected_item
63
- return nil if @queues.empty? || @selected_row >= @queues.size
64
- @queues[@selected_row]
243
+ def exit_detail_mode
244
+ @mode = :list
245
+ @selected_queue = nil
246
+ :exit_queue
65
247
  end
66
248
 
67
- def bindings
68
- [
69
- { key: "j/k", action: "Navigate" },
70
- { key: "G/g", action: "Bottom/Top" }
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 breadcrumb = "queues"
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
- private
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
- 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)
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
- def jump_to_top
85
- @selected_row = 0
86
- @table_state.select(0)
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 jump_to_bottom
90
- return if @queues.empty?
91
- @selected_row = @queues.size - 1
92
- @table_state.select(@selected_row)
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