solid_queue_tui 0.1.1 → 0.1.2

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/exe/sqtui +20 -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 +73 -17
  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 +14 -2
  14. data/lib/solid_queue_tui/data/failed_query.rb +37 -91
  15. data/lib/solid_queue_tui/data/jobs_query.rb +90 -123
  16. data/lib/solid_queue_tui/data/processes_query.rb +10 -34
  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/railtie.rb +9 -0
  21. data/lib/solid_queue_tui/version.rb +1 -1
  22. data/lib/solid_queue_tui/views/blocked_view.rb +126 -46
  23. data/lib/solid_queue_tui/views/concerns/filterable.rb +128 -0
  24. data/lib/solid_queue_tui/views/dashboard_view.rb +2 -1
  25. data/lib/solid_queue_tui/views/failed_view.rb +62 -72
  26. data/lib/solid_queue_tui/views/finished_view.rb +54 -67
  27. data/lib/solid_queue_tui/views/in_progress_view.rb +124 -44
  28. data/lib/solid_queue_tui/views/queues_view.rb +119 -35
  29. data/lib/solid_queue_tui/views/recurring_tasks_view.rb +202 -0
  30. data/lib/solid_queue_tui/views/scheduled_view.rb +74 -8
  31. data/lib/solid_queue_tui.rb +15 -4
  32. data/lib/tasks/solid_queue_tui.rake +8 -0
  33. metadata +16 -24
  34. data/lib/solid_queue_tui/connection.rb +0 -58
@@ -3,42 +3,69 @@
3
3
  module SolidQueueTui
4
4
  module Views
5
5
  class FinishedView
6
+ include Filterable
7
+
8
+
9
+ LOAD_THRESHOLD = 10
10
+
6
11
  def initialize(tui)
7
12
  @tui = tui
8
13
  @table_state = RatatuiRuby::TableState.new(nil)
9
14
  @table_state.select(0)
10
15
  @selected_row = 0
11
16
  @jobs = []
12
- @filter = nil
13
- @filter_mode = false
14
- @filter_input = ""
17
+ @total_count = nil
18
+ @all_loaded = false
19
+ init_filter
15
20
  end
16
21
 
17
22
  def update(jobs:)
18
23
  @jobs = jobs
24
+ @all_loaded = jobs.size < SolidQueueTui.page_size
19
25
  @selected_row = @selected_row.clamp(0, [@jobs.size - 1, 0].max)
20
26
  @table_state.select(@selected_row)
21
27
  end
22
28
 
29
+ def append(jobs:)
30
+ @jobs.concat(jobs)
31
+ @all_loaded = jobs.size < SolidQueueTui.page_size
32
+ end
33
+
34
+ def total_count=(count)
35
+ @total_count = count
36
+ end
37
+
38
+ def current_offset
39
+ @jobs.size
40
+ end
41
+
42
+ def reset_pagination!
43
+ @jobs = []
44
+ @total_count = nil
45
+ @all_loaded = false
46
+ @selected_row = 0
47
+ @table_state.select(0)
48
+ end
49
+
23
50
  def render(frame, area)
24
- if @filter_mode
25
- content_area, filter_area = @tui.layout_split(
51
+ if filter_mode?
52
+ filter_area, content_area = @tui.layout_split(
26
53
  area,
27
54
  direction: :vertical,
28
55
  constraints: [
29
- @tui.constraint_fill(1),
30
- @tui.constraint_length(3)
56
+ @tui.constraint_length(3),
57
+ @tui.constraint_fill(1)
31
58
  ]
32
59
  )
33
- render_table(frame, content_area)
34
60
  render_filter_input(frame, filter_area)
61
+ render_table(frame, content_area)
35
62
  else
36
63
  render_table(frame, area)
37
64
  end
38
65
  end
39
66
 
40
67
  def handle_input(event)
41
- if @filter_mode
68
+ if filter_mode?
42
69
  handle_filter_input(event)
43
70
  else
44
71
  handle_normal_input(event)
@@ -50,14 +77,9 @@ module SolidQueueTui
50
77
  @jobs[@selected_row]
51
78
  end
52
79
 
53
- def filter = @filter
54
-
55
80
  def bindings
56
- if @filter_mode
57
- [
58
- { key: "Enter", action: "Apply" },
59
- { key: "Esc", action: "Cancel" }
60
- ]
81
+ if filter_mode?
82
+ filter_bindings
61
83
  else
62
84
  [
63
85
  { key: "j/k", action: "Navigate" },
@@ -70,55 +92,36 @@ module SolidQueueTui
70
92
  end
71
93
 
72
94
  def capturing_input?
73
- @filter_mode
95
+ filter_mode?
74
96
  end
75
97
 
76
98
  def breadcrumb
77
- @filter ? "finished:#{@filter}" : "finished"
99
+ @filters.empty? ? "finished" : "finished:filtered"
78
100
  end
79
101
 
80
102
  private
81
103
 
104
+ def needs_more?
105
+ !@all_loaded && @selected_row >= @jobs.size - LOAD_THRESHOLD
106
+ end
107
+
82
108
  def handle_normal_input(event)
83
109
  case event
84
110
  in { type: :key, code: "j" } | { type: :key, code: "up" }
85
111
  move_selection(-1)
86
112
  in { type: :key, code: "k" } | { type: :key, code: "down" }
87
- move_selection(1)
113
+ result = move_selection(1)
114
+ return :load_more if result == :load_more
115
+ nil
88
116
  in { type: :key, code: "g" }
89
117
  jump_to_top
90
118
  in { type: :key, code: "G" }
91
119
  jump_to_bottom
92
120
  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
121
+ enter_filter_mode
100
122
  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
123
  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
124
+ clear_filter
122
125
  else
123
126
  nil
124
127
  end
@@ -128,6 +131,7 @@ module SolidQueueTui
128
131
  return if @jobs.empty?
129
132
  @selected_row = (@selected_row + delta).clamp(0, @jobs.size - 1)
130
133
  @table_state.select(@selected_row)
134
+ :load_more if needs_more?
131
135
  end
132
136
 
133
137
  def jump_to_top
@@ -139,6 +143,7 @@ module SolidQueueTui
139
143
  return if @jobs.empty?
140
144
  @selected_row = @jobs.size - 1
141
145
  @table_state.select(@selected_row)
146
+ return :load_more if needs_more?
142
147
  end
143
148
 
144
149
  def render_table(frame, area)
@@ -162,37 +167,19 @@ module SolidQueueTui
162
167
  }
163
168
  end
164
169
 
165
- title = @filter ? "Finished (filter: #{@filter})" : "Finished"
166
-
167
170
  table = Components::JobTable.new(
168
171
  @tui,
169
- title: title,
172
+ title: filter_title("Finished"),
170
173
  columns: columns,
171
174
  rows: rows,
172
175
  selected_row: @selected_row,
173
- empty_message: @filter ? "No finished jobs matching '#{@filter}'" : "No finished jobs"
176
+ total_count: @total_count,
177
+ empty_message: @filters.empty? ? "No finished jobs" : "No finished jobs matching filters"
174
178
  )
175
179
 
176
180
  table.render(frame, area, @table_state)
177
181
  end
178
182
 
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
183
  def format_time(time)
197
184
  return "n/a" unless time
198
185
  time.strftime("%Y-%m-%d %H:%M:%S")
@@ -3,65 +3,72 @@
3
3
  module SolidQueueTui
4
4
  module Views
5
5
  class InProgressView
6
+ include Filterable
7
+
8
+
9
+ LOAD_THRESHOLD = 10
10
+
6
11
  def initialize(tui)
7
12
  @tui = tui
8
13
  @table_state = RatatuiRuby::TableState.new(nil)
9
14
  @table_state.select(0)
10
15
  @selected_row = 0
11
16
  @jobs = []
17
+ @total_count = nil
18
+ @all_loaded = false
19
+ init_filter
12
20
  end
13
21
 
14
22
  def update(jobs:)
15
23
  @jobs = jobs
24
+ @all_loaded = jobs.size < SolidQueueTui.page_size
16
25
  @selected_row = @selected_row.clamp(0, [@jobs.size - 1, 0].max)
17
26
  @table_state.select(@selected_row)
18
27
  end
19
28
 
20
- def render(frame, area)
21
- columns = [
22
- { key: :id, label: "ID", width: 8 },
23
- { key: :queue_name, label: "QUEUE", width: 14 },
24
- { key: :class_name, label: "CLASS", width: :fill },
25
- { key: :priority, label: "PRI", width: 5 },
26
- { key: :worker_id, label: "WORKER", width: 8 },
27
- { key: :started_at, label: "STARTED", width: 12 }
28
- ]
29
+ def append(jobs:)
30
+ @jobs.concat(jobs)
31
+ @all_loaded = jobs.size < SolidQueueTui.page_size
32
+ end
29
33
 
30
- rows = @jobs.map do |job|
31
- {
32
- id: job.id,
33
- queue_name: job.queue_name,
34
- class_name: job.class_name,
35
- priority: job.priority,
36
- worker_id: job.worker_id || "n/a",
37
- started_at: time_ago(job.started_at)
38
- }
39
- end
34
+ def total_count=(count)
35
+ @total_count = count
36
+ end
40
37
 
41
- table = Components::JobTable.new(
42
- @tui,
43
- title: "In Progress",
44
- columns: columns,
45
- rows: rows,
46
- selected_row: @selected_row,
47
- empty_message: "No jobs currently in progress"
48
- )
38
+ def current_offset
39
+ @jobs.size
40
+ end
49
41
 
50
- table.render(frame, area, @table_state)
42
+ def reset_pagination!
43
+ @jobs = []
44
+ @total_count = nil
45
+ @all_loaded = false
46
+ @selected_row = 0
47
+ @table_state.select(0)
48
+ end
49
+
50
+ def render(frame, area)
51
+ if filter_mode?
52
+ filter_area, content_area = @tui.layout_split(
53
+ area,
54
+ direction: :vertical,
55
+ constraints: [
56
+ @tui.constraint_length(3),
57
+ @tui.constraint_fill(1)
58
+ ]
59
+ )
60
+ render_filter_input(frame, filter_area)
61
+ render_table(frame, content_area)
62
+ else
63
+ render_table(frame, area)
64
+ end
51
65
  end
52
66
 
53
67
  def handle_input(event)
54
- case event
55
- in { type: :key, code: "j" } | { type: :key, code: "up" }
56
- move_selection(-1)
57
- in { type: :key, code: "k" } | { type: :key, code: "down" }
58
- move_selection(1)
59
- in { type: :key, code: "g" }
60
- jump_to_top
61
- in { type: :key, code: "G" }
62
- jump_to_bottom
68
+ if filter_mode?
69
+ handle_filter_input(event)
63
70
  else
64
- nil
71
+ handle_normal_input(event)
65
72
  end
66
73
  end
67
74
 
@@ -71,21 +78,59 @@ module SolidQueueTui
71
78
  end
72
79
 
73
80
  def bindings
74
- [
75
- { key: "j/k", action: "Navigate" },
76
- { key: "Enter", action: "Detail" },
77
- { key: "G/g", action: "Bottom/Top" }
78
- ]
81
+ if filter_mode?
82
+ filter_bindings
83
+ else
84
+ [
85
+ { key: "j/k", action: "Navigate" },
86
+ { key: "Enter", action: "Detail" },
87
+ { key: "/", action: "Filter" },
88
+ { key: "G/g", action: "Bottom/Top" }
89
+ ]
90
+ end
91
+ end
92
+
93
+ def capturing_input?
94
+ filter_mode?
79
95
  end
80
96
 
81
- def breadcrumb = "in-progress"
97
+ def breadcrumb
98
+ @filters.empty? ? "in-progress" : "in-progress:filtered"
99
+ end
82
100
 
83
101
  private
84
102
 
103
+ def needs_more?
104
+ !@all_loaded && @selected_row >= @jobs.size - LOAD_THRESHOLD
105
+ end
106
+
107
+ def handle_normal_input(event)
108
+ case event
109
+ in { type: :key, code: "j" } | { type: :key, code: "up" }
110
+ move_selection(-1)
111
+ in { type: :key, code: "k" } | { type: :key, code: "down" }
112
+ result = move_selection(1)
113
+ return :load_more if result == :load_more
114
+ nil
115
+ in { type: :key, code: "g" }
116
+ jump_to_top
117
+ in { type: :key, code: "G" }
118
+ jump_to_bottom
119
+ in { type: :key, code: "/" }
120
+ enter_filter_mode
121
+ nil
122
+ in { type: :key, code: "esc" }
123
+ clear_filter
124
+ else
125
+ nil
126
+ end
127
+ end
128
+
85
129
  def move_selection(delta)
86
130
  return if @jobs.empty?
87
131
  @selected_row = (@selected_row + delta).clamp(0, @jobs.size - 1)
88
132
  @table_state.select(@selected_row)
133
+ :load_more if needs_more?
89
134
  end
90
135
 
91
136
  def jump_to_top
@@ -97,6 +142,41 @@ module SolidQueueTui
97
142
  return if @jobs.empty?
98
143
  @selected_row = @jobs.size - 1
99
144
  @table_state.select(@selected_row)
145
+ return :load_more if needs_more?
146
+ end
147
+
148
+ def render_table(frame, area)
149
+ columns = [
150
+ { key: :id, label: "ID", width: 8 },
151
+ { key: :queue_name, label: "QUEUE", width: 14 },
152
+ { key: :class_name, label: "CLASS", width: :fill },
153
+ { key: :priority, label: "PRI", width: 5 },
154
+ { key: :worker_id, label: "WORKER", width: 8 },
155
+ { key: :started_at, label: "STARTED", width: 12 }
156
+ ]
157
+
158
+ rows = @jobs.map do |job|
159
+ {
160
+ id: job.id,
161
+ queue_name: job.queue_name,
162
+ class_name: job.class_name,
163
+ priority: job.priority,
164
+ worker_id: job.worker_id || "n/a",
165
+ started_at: time_ago(job.started_at)
166
+ }
167
+ end
168
+
169
+ table = Components::JobTable.new(
170
+ @tui,
171
+ title: filter_title("In Progress"),
172
+ columns: columns,
173
+ rows: rows,
174
+ selected_row: @selected_row,
175
+ total_count: @total_count,
176
+ empty_message: @filters.empty? ? "No jobs currently in progress" : "No in-progress jobs matching filters"
177
+ )
178
+
179
+ table.render(frame, area, @table_state)
100
180
  end
101
181
 
102
182
  def time_ago(time)
@@ -9,6 +9,7 @@ module SolidQueueTui
9
9
  @table_state.select(0)
10
10
  @selected_row = 0
11
11
  @queues = []
12
+ @confirm_action = nil
12
13
  end
13
14
 
14
15
  def update(queues:)
@@ -18,33 +19,59 @@ module SolidQueueTui
18
19
  end
19
20
 
20
21
  def render(frame, area)
21
- columns = [
22
- { key: :name, label: "QUEUE", width: :fill },
23
- { key: :size, label: "SIZE", width: 10 },
24
- { key: :status, label: "STATUS", width: 10, color_by: :status }
25
- ]
22
+ if @confirm_action
23
+ confirm_area, content_area = @tui.layout_split(
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)
33
+ else
34
+ render_table(frame, area)
35
+ end
36
+ end
26
37
 
27
- rows = @queues.map do |q|
28
- {
29
- name: q.name,
30
- size: q.size,
31
- status: q.paused ? "paused" : "active"
32
- }
38
+ def handle_input(event)
39
+ if @confirm_action
40
+ handle_confirm_input(event)
41
+ else
42
+ handle_normal_input(event)
33
43
  end
44
+ end
34
45
 
35
- table = Components::JobTable.new(
36
- @tui,
37
- title: "Queues",
38
- columns: columns,
39
- rows: rows,
40
- selected_row: @selected_row,
41
- empty_message: "No queues found"
42
- )
46
+ def selected_item
47
+ return nil if @queues.empty? || @selected_row >= @queues.size
48
+ @queues[@selected_row]
49
+ end
43
50
 
44
- table.render(frame, area, @table_state)
51
+ def capturing_input?
52
+ !!@confirm_action
45
53
  end
46
54
 
47
- def handle_input(event)
55
+ def bindings
56
+ if @confirm_action
57
+ [
58
+ { key: "y", action: "Confirm" },
59
+ { key: "n/Esc", action: "Cancel" }
60
+ ]
61
+ else
62
+ [
63
+ { key: "j/k", action: "Navigate" },
64
+ { key: "p", action: "Pause/Resume" },
65
+ { key: "G/g", action: "Bottom/Top" }
66
+ ]
67
+ end
68
+ end
69
+
70
+ def breadcrumb = "queues"
71
+
72
+ private
73
+
74
+ def handle_normal_input(event)
48
75
  case event
49
76
  in { type: :key, code: "j" } | { type: :key, code: "up" }
50
77
  move_selection(-1)
@@ -54,27 +81,33 @@ module SolidQueueTui
54
81
  jump_to_top
55
82
  in { type: :key, code: "G" }
56
83
  jump_to_bottom
84
+ in { type: :key, code: "p" }
85
+ queue = selected_item
86
+ if queue
87
+ @confirm_action = queue.paused ? :resume : :pause
88
+ end
89
+ nil
57
90
  else
58
91
  nil
59
92
  end
60
93
  end
61
94
 
62
- def selected_item
63
- return nil if @queues.empty? || @selected_row >= @queues.size
64
- @queues[@selected_row]
65
- end
66
-
67
- def bindings
68
- [
69
- { key: "j/k", action: "Navigate" },
70
- { key: "G/g", action: "Bottom/Top" }
71
- ]
95
+ def handle_confirm_input(event)
96
+ case event
97
+ in { type: :key, code: "y" }
98
+ @confirm_action = nil
99
+ queue = selected_item
100
+ return nil unless queue
101
+ Actions::ToggleQueuePause.call(queue.name)
102
+ :refresh
103
+ in { type: :key, code: "n" } | { type: :key, code: "esc" }
104
+ @confirm_action = nil
105
+ nil
106
+ else
107
+ nil
108
+ end
72
109
  end
73
110
 
74
- def breadcrumb = "queues"
75
-
76
- private
77
-
78
111
  def move_selection(delta)
79
112
  return if @queues.empty?
80
113
  @selected_row = (@selected_row + delta).clamp(0, @queues.size - 1)
@@ -91,6 +124,57 @@ module SolidQueueTui
91
124
  @selected_row = @queues.size - 1
92
125
  @table_state.select(@selected_row)
93
126
  end
127
+
128
+ def render_table(frame, area)
129
+ columns = [
130
+ { key: :name, label: "QUEUE", width: :fill },
131
+ { key: :size, label: "SIZE", width: 10 },
132
+ { key: :status, label: "STATUS", width: 10, color_by: :status }
133
+ ]
134
+
135
+ rows = @queues.map do |q|
136
+ {
137
+ name: q.name,
138
+ size: q.size,
139
+ status: q.paused ? "paused" : "active"
140
+ }
141
+ end
142
+
143
+ table = Components::JobTable.new(
144
+ @tui,
145
+ title: "Queues",
146
+ columns: columns,
147
+ rows: rows,
148
+ selected_row: @selected_row,
149
+ empty_message: "No queues found"
150
+ )
151
+
152
+ table.render(frame, area, @table_state)
153
+ end
154
+
155
+ def render_confirm(frame, area)
156
+ queue = selected_item
157
+ message = if @confirm_action == :pause
158
+ "Pause queue '#{queue&.name}'? Workers will stop picking up jobs from this queue. [y/n]"
159
+ else
160
+ "Resume queue '#{queue&.name}'? [y/n]"
161
+ end
162
+
163
+ frame.render_widget(
164
+ @tui.paragraph(
165
+ text: " #{message}",
166
+ style: @tui.style(fg: :yellow, modifiers: [:bold]),
167
+ block: @tui.block(
168
+ title: " Confirm ",
169
+ title_style: @tui.style(fg: :red, modifiers: [:bold]),
170
+ borders: [:all],
171
+ border_type: :rounded,
172
+ border_style: @tui.style(fg: :red)
173
+ )
174
+ ),
175
+ area
176
+ )
177
+ end
94
178
  end
95
179
  end
96
180
  end