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,80 +3,61 @@
3
3
  module SolidQueueTui
4
4
  module Views
5
5
  class FailedView
6
+ include Filterable
7
+ include Confirmable
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
11
- @failed_jobs = []
12
- @filter = nil
13
- @filter_mode = false
14
- @filter_input = ""
15
- @confirm_action = nil
13
+ init_pagination
14
+ init_confirm
15
+ init_filter
16
16
  end
17
17
 
18
18
  def update(failed_jobs:)
19
- @failed_jobs = failed_jobs
20
- @selected_row = @selected_row.clamp(0, [@failed_jobs.size - 1, 0].max)
21
- @table_state.select(@selected_row)
19
+ update_items(failed_jobs)
20
+ end
21
+
22
+ def append(failed_jobs:)
23
+ append_items(failed_jobs)
22
24
  end
23
25
 
24
26
  def render(frame, area)
25
- if @confirm_action
26
- content_area, confirm_area = @tui.layout_split(
27
- area,
28
- direction: :vertical,
29
- constraints: [
30
- @tui.constraint_fill(1),
31
- @tui.constraint_length(3)
32
- ]
33
- )
34
- render_failed_table(frame, content_area)
35
- render_confirm(frame, confirm_area)
36
- elsif @filter_mode
37
- content_area, filter_area = @tui.layout_split(
27
+ if confirm_mode?
28
+ render_failed_table(frame, area)
29
+ render_confirm_popup(frame, area)
30
+ elsif filter_mode?
31
+ filter_area, content_area = @tui.layout_split(
38
32
  area,
39
33
  direction: :vertical,
40
34
  constraints: [
41
- @tui.constraint_fill(1),
42
- @tui.constraint_length(3)
35
+ @tui.constraint_length(3),
36
+ @tui.constraint_fill(1)
43
37
  ]
44
38
  )
45
- render_failed_table(frame, content_area)
46
39
  render_filter_input(frame, filter_area)
40
+ render_failed_table(frame, content_area)
47
41
  else
48
42
  render_failed_table(frame, area)
49
43
  end
50
44
  end
51
45
 
52
46
  def handle_input(event)
53
- if @confirm_action
47
+ if confirm_mode?
54
48
  handle_confirm_input(event)
55
- elsif @filter_mode
49
+ elsif filter_mode?
56
50
  handle_filter_input(event)
57
51
  else
58
52
  handle_normal_input(event)
59
53
  end
60
54
  end
61
55
 
62
- def selected_item
63
- return nil if @failed_jobs.empty? || @selected_row >= @failed_jobs.size
64
- @failed_jobs[@selected_row]
65
- end
66
-
67
- def filter = @filter
68
-
69
56
  def bindings
70
- if @confirm_action
71
- [
72
- { key: "y", action: "Confirm" },
73
- { key: "n/Esc", action: "Cancel" }
74
- ]
75
- elsif @filter_mode
76
- [
77
- { key: "Enter", action: "Apply" },
78
- { key: "Esc", action: "Cancel" }
79
- ]
57
+ if confirm_mode?
58
+ confirm_bindings
59
+ elsif filter_mode?
60
+ filter_bindings
80
61
  else
81
62
  [
82
63
  { key: "j/k", action: "Navigate" },
@@ -90,11 +71,11 @@ module SolidQueueTui
90
71
  end
91
72
 
92
73
  def capturing_input?
93
- @filter_mode || @confirm_action
74
+ filter_mode? || confirm_mode?
94
75
  end
95
76
 
96
77
  def breadcrumb
97
- @filter ? "failed:#{@filter}" : "failed"
78
+ @filters.empty? ? "failed" : "failed:filtered"
98
79
  end
99
80
 
100
81
  private
@@ -104,7 +85,9 @@ module SolidQueueTui
104
85
  in { type: :key, code: "j" } | { type: :key, code: "up" }
105
86
  move_selection(-1)
106
87
  in { type: :key, code: "k" } | { type: :key, code: "down" }
107
- move_selection(1)
88
+ result = move_selection(1)
89
+ return :load_more if result == :load_more
90
+ nil
108
91
  in { type: :key, code: "g" }
109
92
  jump_to_top
110
93
  in { type: :key, code: "G" }
@@ -116,88 +99,52 @@ module SolidQueueTui
116
99
  @confirm_action = :discard if selected_item
117
100
  nil
118
101
  in { type: :key, code: "A" }
119
- @confirm_action = :retry_all unless @failed_jobs.empty?
102
+ @confirm_action = :retry_all unless items.empty?
120
103
  nil
121
104
  in { type: :key, code: "/" }
122
- @filter_mode = true
123
- @filter_input = @filter || ""
105
+ enter_filter_mode
124
106
  nil
125
107
  in { type: :key, code: "esc" }
126
- @filter = nil
127
- @filter_input = ""
128
- :refresh
108
+ clear_filter
129
109
  else
130
110
  nil
131
111
  end
132
112
  end
133
113
 
134
- def handle_confirm_input(event)
135
- case event
136
- in { type: :key, code: "y" }
137
- action = @confirm_action
138
- @confirm_action = nil
139
- case action
140
- when :retry
141
- item = selected_item
142
- return nil unless item
143
- Actions::RetryJob.call(item.id)
144
- :refresh
145
- when :discard
146
- item = selected_item
147
- return nil unless item
148
- Actions::DiscardJob.call(item.id)
149
- :refresh
150
- when :retry_all
151
- Actions::RetryJob.retry_all
152
- :refresh
153
- end
154
- in { type: :key, code: "n" } | { type: :key, code: "esc" }
155
- @confirm_action = nil
156
- nil
157
- else
158
- nil
114
+ def confirm_message
115
+ case @confirm_action
116
+ when :retry
117
+ job = selected_item
118
+ "Retry job ##{job&.job_id} (#{job&.class_name})? [y/n]"
119
+ when :discard
120
+ job = selected_item
121
+ "Discard job ##{job&.job_id} (#{job&.class_name})? This cannot be undone. [y/n]"
122
+ when :retry_all
123
+ count = @total_count || items.size
124
+ label = @filters.empty? ? "failed jobs" : "filtered failed jobs"
125
+ "Retry ALL #{count} #{label}? [y/n]"
159
126
  end
160
127
  end
161
128
 
162
- def handle_filter_input(event)
163
- case event
164
- in { type: :key, code: "enter" }
165
- @filter = @filter_input.empty? ? nil : @filter_input
166
- @filter_mode = false
167
- @selected_row = 0
168
- @table_state.select(0)
129
+ def execute_confirm_action(action)
130
+ case action
131
+ when :retry
132
+ item = selected_item
133
+ return nil unless item
134
+ Actions::RetryJob.call(item.id)
135
+ :refresh
136
+ when :discard
137
+ item = selected_item
138
+ return nil unless item
139
+ Actions::DiscardJob.call(item.id)
140
+ :refresh
141
+ when :retry_all
142
+ f = filters
143
+ Actions::RetryJob.retry_all(filter: f[:class_name], queue: f[:queue])
169
144
  :refresh
170
- in { type: :key, code: "esc" }
171
- @filter_mode = false
172
- nil
173
- in { type: :key, code: "backspace" }
174
- @filter_input = @filter_input[0...-1]
175
- nil
176
- in { type: :key, code: /\A.\z/ => char }
177
- @filter_input += char
178
- nil
179
- else
180
- nil
181
145
  end
182
146
  end
183
147
 
184
- def move_selection(delta)
185
- return if @failed_jobs.empty?
186
- @selected_row = (@selected_row + delta).clamp(0, @failed_jobs.size - 1)
187
- @table_state.select(@selected_row)
188
- end
189
-
190
- def jump_to_top
191
- @selected_row = 0
192
- @table_state.select(0)
193
- end
194
-
195
- def jump_to_bottom
196
- return if @failed_jobs.empty?
197
- @selected_row = @failed_jobs.size - 1
198
- @table_state.select(@selected_row)
199
- end
200
-
201
148
  def render_failed_table(frame, area)
202
149
  columns = [
203
150
  { key: :id, label: "ID", width: 8 },
@@ -208,7 +155,7 @@ module SolidQueueTui
208
155
  { key: :failed_at, label: "FAILED", width: 12 }
209
156
  ]
210
157
 
211
- rows = @failed_jobs.map do |job|
158
+ rows = items.map do |job|
212
159
  {
213
160
  id: job.job_id,
214
161
  class_name: job.class_name,
@@ -219,80 +166,19 @@ module SolidQueueTui
219
166
  }
220
167
  end
221
168
 
222
- title = @filter ? "Failed Jobs (filter: #{@filter})" : "Failed Jobs"
223
-
224
169
  table = Components::JobTable.new(
225
170
  @tui,
226
- title: title,
171
+ title: filter_title("Failed Jobs"),
227
172
  columns: columns,
228
173
  rows: rows,
229
174
  selected_row: @selected_row,
175
+ total_count: @total_count,
230
176
  empty_message: "No failed jobs — everything is running smoothly!"
231
177
  )
232
178
 
233
179
  table.render(frame, area, @table_state)
234
180
  end
235
181
 
236
- def render_confirm(frame, area)
237
- message = case @confirm_action
238
- when :retry
239
- job = selected_item
240
- "Retry job ##{job&.job_id} (#{job&.class_name})? [y/n]"
241
- when :discard
242
- job = selected_item
243
- "Discard job ##{job&.job_id} (#{job&.class_name})? This cannot be undone. [y/n]"
244
- when :retry_all
245
- "Retry ALL #{@failed_jobs.size} failed jobs? [y/n]"
246
- end
247
-
248
- frame.render_widget(
249
- @tui.paragraph(
250
- text: " #{message}",
251
- style: @tui.style(fg: :yellow, modifiers: [:bold]),
252
- block: @tui.block(
253
- title: " Confirm ",
254
- title_style: @tui.style(fg: :red, modifiers: [:bold]),
255
- borders: [:all],
256
- border_type: :rounded,
257
- border_style: @tui.style(fg: :red)
258
- )
259
- ),
260
- area
261
- )
262
- end
263
-
264
- def render_filter_input(frame, area)
265
- frame.render_widget(
266
- @tui.paragraph(
267
- text: @filter_input + "█",
268
- style: @tui.style(fg: :white),
269
- block: @tui.block(
270
- title: " Filter by class name ",
271
- title_style: @tui.style(fg: :yellow),
272
- borders: [:all],
273
- border_type: :rounded,
274
- border_style: @tui.style(fg: :cyan)
275
- )
276
- ),
277
- area
278
- )
279
- end
280
-
281
- def truncate(str, max)
282
- return "" unless str
283
- str.length > max ? "#{str[0...max - 3]}..." : str
284
- end
285
-
286
- def time_ago(time)
287
- return "n/a" unless time
288
- seconds = (Time.now.utc - time).to_i
289
- case seconds
290
- when 0..59 then "#{seconds}s ago"
291
- when 60..3599 then "#{seconds / 60}m ago"
292
- when 3600..86399 then "#{seconds / 3600}h ago"
293
- else "#{seconds / 86400}d ago"
294
- end
295
- end
296
182
  end
297
183
  end
298
184
  end
@@ -3,61 +3,52 @@
3
3
  module SolidQueueTui
4
4
  module Views
5
5
  class FinishedView
6
+ include Filterable
7
+ include Paginatable
8
+ include FormattingHelpers
9
+
6
10
  def initialize(tui)
7
11
  @tui = tui
8
- @table_state = RatatuiRuby::TableState.new(nil)
9
- @table_state.select(0)
10
- @selected_row = 0
11
- @jobs = []
12
- @filter = nil
13
- @filter_mode = false
14
- @filter_input = ""
12
+ init_pagination
13
+ init_filter
15
14
  end
16
15
 
17
16
  def update(jobs:)
18
- @jobs = jobs
19
- @selected_row = @selected_row.clamp(0, [@jobs.size - 1, 0].max)
20
- @table_state.select(@selected_row)
17
+ update_items(jobs)
18
+ end
19
+
20
+ def append(jobs:)
21
+ append_items(jobs)
21
22
  end
22
23
 
23
24
  def render(frame, area)
24
- if @filter_mode
25
- content_area, filter_area = @tui.layout_split(
25
+ if filter_mode?
26
+ filter_area, content_area = @tui.layout_split(
26
27
  area,
27
28
  direction: :vertical,
28
29
  constraints: [
29
- @tui.constraint_fill(1),
30
- @tui.constraint_length(3)
30
+ @tui.constraint_length(3),
31
+ @tui.constraint_fill(1)
31
32
  ]
32
33
  )
33
- render_table(frame, content_area)
34
34
  render_filter_input(frame, filter_area)
35
+ render_table(frame, content_area)
35
36
  else
36
37
  render_table(frame, area)
37
38
  end
38
39
  end
39
40
 
40
41
  def handle_input(event)
41
- if @filter_mode
42
+ if filter_mode?
42
43
  handle_filter_input(event)
43
44
  else
44
45
  handle_normal_input(event)
45
46
  end
46
47
  end
47
48
 
48
- def selected_item
49
- return nil if @jobs.empty? || @selected_row >= @jobs.size
50
- @jobs[@selected_row]
51
- end
52
-
53
- def filter = @filter
54
-
55
49
  def bindings
56
- if @filter_mode
57
- [
58
- { key: "Enter", action: "Apply" },
59
- { key: "Esc", action: "Cancel" }
60
- ]
50
+ if filter_mode?
51
+ filter_bindings
61
52
  else
62
53
  [
63
54
  { key: "j/k", action: "Navigate" },
@@ -70,11 +61,11 @@ module SolidQueueTui
70
61
  end
71
62
 
72
63
  def capturing_input?
73
- @filter_mode
64
+ filter_mode?
74
65
  end
75
66
 
76
67
  def breadcrumb
77
- @filter ? "finished:#{@filter}" : "finished"
68
+ @filters.empty? ? "finished" : "finished:filtered"
78
69
  end
79
70
 
80
71
  private
@@ -84,63 +75,23 @@ module SolidQueueTui
84
75
  in { type: :key, code: "j" } | { type: :key, code: "up" }
85
76
  move_selection(-1)
86
77
  in { type: :key, code: "k" } | { type: :key, code: "down" }
87
- move_selection(1)
78
+ result = move_selection(1)
79
+ return :load_more if result == :load_more
80
+ nil
88
81
  in { type: :key, code: "g" }
89
82
  jump_to_top
90
83
  in { type: :key, code: "G" }
91
84
  jump_to_bottom
92
85
  in { type: :key, code: "/" }
93
- @filter_mode = true
94
- @filter_input = @filter || ""
95
- in { type: :key, code: "esc" }
96
- @filter = nil
97
- @filter_input = ""
98
- :refresh
99
- else
86
+ enter_filter_mode
100
87
  nil
101
- end
102
- end
103
-
104
- def handle_filter_input(event)
105
- case event
106
- in { type: :key, code: "enter" }
107
- @filter = @filter_input.empty? ? nil : @filter_input
108
- @filter_mode = false
109
- @selected_row = 0
110
- @table_state.select(0)
111
- :refresh
112
88
  in { type: :key, code: "esc" }
113
- @filter_mode = false
114
- @filter_input = @filter || ""
115
- nil
116
- in { type: :key, code: "backspace" }
117
- @filter_input = @filter_input[0...-1]
118
- nil
119
- in { type: :key, code: /\A.\z/ => char }
120
- @filter_input += char
121
- nil
89
+ clear_filter
122
90
  else
123
91
  nil
124
92
  end
125
93
  end
126
94
 
127
- def move_selection(delta)
128
- return if @jobs.empty?
129
- @selected_row = (@selected_row + delta).clamp(0, @jobs.size - 1)
130
- @table_state.select(@selected_row)
131
- end
132
-
133
- def jump_to_top
134
- @selected_row = 0
135
- @table_state.select(0)
136
- end
137
-
138
- def jump_to_bottom
139
- return if @jobs.empty?
140
- @selected_row = @jobs.size - 1
141
- @table_state.select(@selected_row)
142
- end
143
-
144
95
  def render_table(frame, area)
145
96
  columns = [
146
97
  { key: :id, label: "ID", width: 8 },
@@ -151,65 +102,33 @@ module SolidQueueTui
151
102
  { key: :duration, label: "DURATION", width: 12 }
152
103
  ]
153
104
 
154
- rows = @jobs.map do |job|
105
+ rows = items.map do |job|
155
106
  {
156
107
  id: job.id,
157
108
  queue_name: job.queue_name,
158
109
  class_name: job.class_name,
159
110
  priority: job.priority,
160
111
  finished_at: format_time(job.finished_at),
161
- duration: format_duration(job.created_at, job.finished_at)
112
+ duration: job_duration(job.created_at, job.finished_at)
162
113
  }
163
114
  end
164
115
 
165
- title = @filter ? "Finished (filter: #{@filter})" : "Finished"
166
-
167
116
  table = Components::JobTable.new(
168
117
  @tui,
169
- title: title,
118
+ title: filter_title("Finished"),
170
119
  columns: columns,
171
120
  rows: rows,
172
121
  selected_row: @selected_row,
173
- empty_message: @filter ? "No finished jobs matching '#{@filter}'" : "No finished jobs"
122
+ total_count: @total_count,
123
+ empty_message: @filters.empty? ? "No finished jobs" : "No finished jobs matching filters"
174
124
  )
175
125
 
176
126
  table.render(frame, area, @table_state)
177
127
  end
178
128
 
179
- def render_filter_input(frame, area)
180
- frame.render_widget(
181
- @tui.paragraph(
182
- text: @filter_input + "\u2588",
183
- style: @tui.style(fg: :white),
184
- block: @tui.block(
185
- title: " Filter by class name ",
186
- title_style: @tui.style(fg: :yellow),
187
- borders: [:all],
188
- border_type: :rounded,
189
- border_style: @tui.style(fg: :cyan)
190
- )
191
- ),
192
- area
193
- )
194
- end
195
-
196
- def format_time(time)
197
- return "n/a" unless time
198
- time.strftime("%Y-%m-%d %H:%M:%S")
199
- end
200
-
201
- def format_duration(created, finished)
129
+ def job_duration(created, finished)
202
130
  return "n/a" unless created && finished
203
- seconds = (finished - created).to_i
204
- if seconds < 1
205
- "<1s"
206
- elsif seconds < 60
207
- "#{seconds}s"
208
- elsif seconds < 3600
209
- "#{seconds / 60}m #{seconds % 60}s"
210
- else
211
- "#{seconds / 3600}h #{(seconds % 3600) / 60}m"
212
- end
131
+ format_duration((finished - created).to_i)
213
132
  end
214
133
  end
215
134
  end