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.
- checksums.yaml +4 -4
- data/exe/sqtui +20 -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 +73 -17
- 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 +14 -2
- data/lib/solid_queue_tui/data/failed_query.rb +37 -91
- data/lib/solid_queue_tui/data/jobs_query.rb +90 -123
- data/lib/solid_queue_tui/data/processes_query.rb +10 -34
- 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/railtie.rb +9 -0
- data/lib/solid_queue_tui/version.rb +1 -1
- data/lib/solid_queue_tui/views/blocked_view.rb +126 -46
- data/lib/solid_queue_tui/views/concerns/filterable.rb +128 -0
- data/lib/solid_queue_tui/views/dashboard_view.rb +2 -1
- data/lib/solid_queue_tui/views/failed_view.rb +62 -72
- data/lib/solid_queue_tui/views/finished_view.rb +54 -67
- data/lib/solid_queue_tui/views/in_progress_view.rb +124 -44
- data/lib/solid_queue_tui/views/queues_view.rb +119 -35
- data/lib/solid_queue_tui/views/recurring_tasks_view.rb +202 -0
- data/lib/solid_queue_tui/views/scheduled_view.rb +74 -8
- data/lib/solid_queue_tui.rb +15 -4
- data/lib/tasks/solid_queue_tui.rake +8 -0
- metadata +16 -24
- data/lib/solid_queue_tui/connection.rb +0 -58
|
@@ -3,67 +3,72 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class BlockedView
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{ key: :class_name, label: "CLASS", width: :fill },
|
|
25
|
-
{ key: :priority, label: "PRI", width: 5 },
|
|
26
|
-
{ key: :concurrency_key, label: "CONCURRENCY KEY", width: :fill },
|
|
27
|
-
{ key: :expires_at, label: "EXPIRES AT", width: 20 },
|
|
28
|
-
{ key: :blocked_since, label: "BLOCKED SINCE", width: 14 }
|
|
29
|
-
]
|
|
29
|
+
def append(jobs:)
|
|
30
|
+
@jobs.concat(jobs)
|
|
31
|
+
@all_loaded = jobs.size < SolidQueueTui.page_size
|
|
32
|
+
end
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
queue_name: job.queue_name,
|
|
35
|
-
class_name: job.class_name,
|
|
36
|
-
priority: job.priority,
|
|
37
|
-
concurrency_key: job.concurrency_key || "n/a",
|
|
38
|
-
expires_at: format_time(job.expires_at),
|
|
39
|
-
blocked_since: time_ago(job.created_at)
|
|
40
|
-
}
|
|
41
|
-
end
|
|
34
|
+
def total_count=(count)
|
|
35
|
+
@total_count = count
|
|
36
|
+
end
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
columns: columns,
|
|
47
|
-
rows: rows,
|
|
48
|
-
selected_row: @selected_row,
|
|
49
|
-
empty_message: "No blocked jobs"
|
|
50
|
-
)
|
|
38
|
+
def current_offset
|
|
39
|
+
@jobs.size
|
|
40
|
+
end
|
|
51
41
|
|
|
52
|
-
|
|
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
|
|
53
65
|
end
|
|
54
66
|
|
|
55
67
|
def handle_input(event)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
move_selection(-1)
|
|
59
|
-
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
60
|
-
move_selection(1)
|
|
61
|
-
in { type: :key, code: "g" }
|
|
62
|
-
jump_to_top
|
|
63
|
-
in { type: :key, code: "G" }
|
|
64
|
-
jump_to_bottom
|
|
68
|
+
if filter_mode?
|
|
69
|
+
handle_filter_input(event)
|
|
65
70
|
else
|
|
66
|
-
|
|
71
|
+
handle_normal_input(event)
|
|
67
72
|
end
|
|
68
73
|
end
|
|
69
74
|
|
|
@@ -73,21 +78,59 @@ module SolidQueueTui
|
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
def bindings
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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?
|
|
81
95
|
end
|
|
82
96
|
|
|
83
|
-
def breadcrumb
|
|
97
|
+
def breadcrumb
|
|
98
|
+
@filters.empty? ? "blocked" : "blocked:filtered"
|
|
99
|
+
end
|
|
84
100
|
|
|
85
101
|
private
|
|
86
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
|
+
|
|
87
129
|
def move_selection(delta)
|
|
88
130
|
return if @jobs.empty?
|
|
89
131
|
@selected_row = (@selected_row + delta).clamp(0, @jobs.size - 1)
|
|
90
132
|
@table_state.select(@selected_row)
|
|
133
|
+
:load_more if needs_more?
|
|
91
134
|
end
|
|
92
135
|
|
|
93
136
|
def jump_to_top
|
|
@@ -99,6 +142,43 @@ module SolidQueueTui
|
|
|
99
142
|
return if @jobs.empty?
|
|
100
143
|
@selected_row = @jobs.size - 1
|
|
101
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: :concurrency_key, label: "CONCURRENCY KEY", width: :fill },
|
|
155
|
+
{ key: :expires_at, label: "EXPIRES AT", width: 20 },
|
|
156
|
+
{ key: :blocked_since, label: "BLOCKED SINCE", width: 14 }
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
rows = @jobs.map do |job|
|
|
160
|
+
{
|
|
161
|
+
id: job.id,
|
|
162
|
+
queue_name: job.queue_name,
|
|
163
|
+
class_name: job.class_name,
|
|
164
|
+
priority: job.priority,
|
|
165
|
+
concurrency_key: job.concurrency_key || "n/a",
|
|
166
|
+
expires_at: format_time(job.expires_at),
|
|
167
|
+
blocked_since: time_ago(job.created_at)
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
table = Components::JobTable.new(
|
|
172
|
+
@tui,
|
|
173
|
+
title: filter_title("Blocked"),
|
|
174
|
+
columns: columns,
|
|
175
|
+
rows: rows,
|
|
176
|
+
selected_row: @selected_row,
|
|
177
|
+
total_count: @total_count,
|
|
178
|
+
empty_message: @filters.empty? ? "No blocked jobs" : "No blocked jobs matching filters"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
table.render(frame, area, @table_state)
|
|
102
182
|
end
|
|
103
183
|
|
|
104
184
|
def format_time(time)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
module Filterable
|
|
6
|
+
FILTER_FIELDS = [
|
|
7
|
+
{ key: :class_name, label: "Class" },
|
|
8
|
+
{ key: :queue, label: "Queue" }
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
def init_filter
|
|
12
|
+
@filters = {}
|
|
13
|
+
@filter_mode = false
|
|
14
|
+
@filter_inputs = {}
|
|
15
|
+
@active_field = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def filters = @filters
|
|
19
|
+
def filter_mode? = @filter_mode
|
|
20
|
+
|
|
21
|
+
def handle_filter_input(event)
|
|
22
|
+
case event
|
|
23
|
+
in { type: :key, code: "enter" }
|
|
24
|
+
@filters = @filter_inputs.reject { |_, v| v.nil? || v.empty? }
|
|
25
|
+
@filter_mode = false
|
|
26
|
+
@selected_row = 0
|
|
27
|
+
@table_state.select(0)
|
|
28
|
+
:refresh
|
|
29
|
+
in { type: :key, code: "esc" }
|
|
30
|
+
@filter_mode = false
|
|
31
|
+
@filter_inputs = @filters.dup
|
|
32
|
+
nil
|
|
33
|
+
in { type: :key, code: "tab" }
|
|
34
|
+
@active_field = (@active_field + 1) % FILTER_FIELDS.size
|
|
35
|
+
nil
|
|
36
|
+
in { type: :key, code: "back_tab" }
|
|
37
|
+
@active_field = (@active_field - 1) % FILTER_FIELDS.size
|
|
38
|
+
nil
|
|
39
|
+
in { type: :key, code: "backspace" }
|
|
40
|
+
field_key = FILTER_FIELDS[@active_field][:key]
|
|
41
|
+
current = @filter_inputs[field_key] || ""
|
|
42
|
+
@filter_inputs[field_key] = current[0...-1]
|
|
43
|
+
nil
|
|
44
|
+
in { type: :key, code: /\A.\z/ => char }
|
|
45
|
+
field_key = FILTER_FIELDS[@active_field][:key]
|
|
46
|
+
@filter_inputs[field_key] = (@filter_inputs[field_key] || "") + char
|
|
47
|
+
nil
|
|
48
|
+
else
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def enter_filter_mode
|
|
54
|
+
@filter_mode = true
|
|
55
|
+
@filter_inputs = @filters.dup
|
|
56
|
+
@active_field = 0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def clear_filter
|
|
60
|
+
@filters = {}
|
|
61
|
+
@filter_inputs = {}
|
|
62
|
+
:refresh
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_filter_input(frame, area)
|
|
66
|
+
spans = []
|
|
67
|
+
|
|
68
|
+
FILTER_FIELDS.each_with_index do |field, idx|
|
|
69
|
+
active = idx == @active_field
|
|
70
|
+
value = @filter_inputs[field[:key]] || ""
|
|
71
|
+
|
|
72
|
+
spans << @tui.text_span(
|
|
73
|
+
content: " #{field[:label]}: ",
|
|
74
|
+
style: @tui.style(fg: active ? :yellow : :dark_gray, modifiers: active ? [:bold] : [])
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if active
|
|
78
|
+
spans << @tui.text_span(content: value + "\u2588", style: @tui.style(fg: :white))
|
|
79
|
+
else
|
|
80
|
+
spans << @tui.text_span(
|
|
81
|
+
content: value.empty? ? "\u2014" : value,
|
|
82
|
+
style: @tui.style(fg: :dark_gray)
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
spans << @tui.text_span(content: " ", style: @tui.style(fg: :white))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
frame.render_widget(
|
|
90
|
+
@tui.paragraph(
|
|
91
|
+
text: @tui.text_line(spans: spans),
|
|
92
|
+
block: @tui.block(
|
|
93
|
+
title: " Filters ",
|
|
94
|
+
title_style: @tui.style(fg: :yellow),
|
|
95
|
+
titles: [
|
|
96
|
+
{ content: " Tab: next field \u2502 Enter: apply \u2502 Esc: cancel ",
|
|
97
|
+
position: :top, alignment: :right }
|
|
98
|
+
],
|
|
99
|
+
borders: [:all],
|
|
100
|
+
border_type: :rounded,
|
|
101
|
+
border_style: @tui.style(fg: :cyan)
|
|
102
|
+
)
|
|
103
|
+
),
|
|
104
|
+
area
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def filter_title(base_title)
|
|
109
|
+
return base_title if @filters.empty?
|
|
110
|
+
|
|
111
|
+
parts = FILTER_FIELDS.filter_map do |field|
|
|
112
|
+
value = @filters[field[:key]]
|
|
113
|
+
"#{field[:label].downcase}: #{value}" if value && !value.empty?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
"#{base_title} (#{parts.join(', ')})"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def filter_bindings
|
|
120
|
+
[
|
|
121
|
+
{ key: "Tab", action: "Next Field" },
|
|
122
|
+
{ key: "Enter", action: "Apply" },
|
|
123
|
+
{ key: "Esc", action: "Cancel" }
|
|
124
|
+
]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -3,47 +3,74 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Views
|
|
5
5
|
class FailedView
|
|
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
|
@failed_jobs = []
|
|
12
|
-
@
|
|
13
|
-
@
|
|
14
|
-
@filter_input = ""
|
|
17
|
+
@total_count = nil
|
|
18
|
+
@all_loaded = false
|
|
15
19
|
@confirm_action = nil
|
|
20
|
+
init_filter
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
def update(failed_jobs:)
|
|
19
24
|
@failed_jobs = failed_jobs
|
|
25
|
+
@all_loaded = failed_jobs.size < SolidQueueTui.page_size
|
|
20
26
|
@selected_row = @selected_row.clamp(0, [@failed_jobs.size - 1, 0].max)
|
|
21
27
|
@table_state.select(@selected_row)
|
|
22
28
|
end
|
|
23
29
|
|
|
30
|
+
def append(failed_jobs:)
|
|
31
|
+
@failed_jobs.concat(failed_jobs)
|
|
32
|
+
@all_loaded = failed_jobs.size < SolidQueueTui.page_size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def total_count=(count)
|
|
36
|
+
@total_count = count
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def current_offset
|
|
40
|
+
@failed_jobs.size
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset_pagination!
|
|
44
|
+
@failed_jobs = []
|
|
45
|
+
@total_count = nil
|
|
46
|
+
@all_loaded = false
|
|
47
|
+
@selected_row = 0
|
|
48
|
+
@table_state.select(0)
|
|
49
|
+
end
|
|
50
|
+
|
|
24
51
|
def render(frame, area)
|
|
25
52
|
if @confirm_action
|
|
26
|
-
|
|
53
|
+
confirm_area, content_area = @tui.layout_split(
|
|
27
54
|
area,
|
|
28
55
|
direction: :vertical,
|
|
29
56
|
constraints: [
|
|
30
|
-
@tui.
|
|
31
|
-
@tui.
|
|
57
|
+
@tui.constraint_length(3),
|
|
58
|
+
@tui.constraint_fill(1)
|
|
32
59
|
]
|
|
33
60
|
)
|
|
34
|
-
render_failed_table(frame, content_area)
|
|
35
61
|
render_confirm(frame, confirm_area)
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
render_failed_table(frame, content_area)
|
|
63
|
+
elsif filter_mode?
|
|
64
|
+
filter_area, content_area = @tui.layout_split(
|
|
38
65
|
area,
|
|
39
66
|
direction: :vertical,
|
|
40
67
|
constraints: [
|
|
41
|
-
@tui.
|
|
42
|
-
@tui.
|
|
68
|
+
@tui.constraint_length(3),
|
|
69
|
+
@tui.constraint_fill(1)
|
|
43
70
|
]
|
|
44
71
|
)
|
|
45
|
-
render_failed_table(frame, content_area)
|
|
46
72
|
render_filter_input(frame, filter_area)
|
|
73
|
+
render_failed_table(frame, content_area)
|
|
47
74
|
else
|
|
48
75
|
render_failed_table(frame, area)
|
|
49
76
|
end
|
|
@@ -52,7 +79,7 @@ module SolidQueueTui
|
|
|
52
79
|
def handle_input(event)
|
|
53
80
|
if @confirm_action
|
|
54
81
|
handle_confirm_input(event)
|
|
55
|
-
elsif
|
|
82
|
+
elsif filter_mode?
|
|
56
83
|
handle_filter_input(event)
|
|
57
84
|
else
|
|
58
85
|
handle_normal_input(event)
|
|
@@ -64,19 +91,14 @@ module SolidQueueTui
|
|
|
64
91
|
@failed_jobs[@selected_row]
|
|
65
92
|
end
|
|
66
93
|
|
|
67
|
-
def filter = @filter
|
|
68
|
-
|
|
69
94
|
def bindings
|
|
70
95
|
if @confirm_action
|
|
71
96
|
[
|
|
72
97
|
{ key: "y", action: "Confirm" },
|
|
73
98
|
{ key: "n/Esc", action: "Cancel" }
|
|
74
99
|
]
|
|
75
|
-
elsif
|
|
76
|
-
|
|
77
|
-
{ key: "Enter", action: "Apply" },
|
|
78
|
-
{ key: "Esc", action: "Cancel" }
|
|
79
|
-
]
|
|
100
|
+
elsif filter_mode?
|
|
101
|
+
filter_bindings
|
|
80
102
|
else
|
|
81
103
|
[
|
|
82
104
|
{ key: "j/k", action: "Navigate" },
|
|
@@ -90,21 +112,27 @@ module SolidQueueTui
|
|
|
90
112
|
end
|
|
91
113
|
|
|
92
114
|
def capturing_input?
|
|
93
|
-
|
|
115
|
+
filter_mode? || @confirm_action
|
|
94
116
|
end
|
|
95
117
|
|
|
96
118
|
def breadcrumb
|
|
97
|
-
@
|
|
119
|
+
@filters.empty? ? "failed" : "failed:filtered"
|
|
98
120
|
end
|
|
99
121
|
|
|
100
122
|
private
|
|
101
123
|
|
|
124
|
+
def needs_more?
|
|
125
|
+
!@all_loaded && @selected_row >= @failed_jobs.size - LOAD_THRESHOLD
|
|
126
|
+
end
|
|
127
|
+
|
|
102
128
|
def handle_normal_input(event)
|
|
103
129
|
case event
|
|
104
130
|
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
105
131
|
move_selection(-1)
|
|
106
132
|
in { type: :key, code: "k" } | { type: :key, code: "down" }
|
|
107
|
-
move_selection(1)
|
|
133
|
+
result = move_selection(1)
|
|
134
|
+
return :load_more if result == :load_more
|
|
135
|
+
nil
|
|
108
136
|
in { type: :key, code: "g" }
|
|
109
137
|
jump_to_top
|
|
110
138
|
in { type: :key, code: "G" }
|
|
@@ -119,13 +147,10 @@ module SolidQueueTui
|
|
|
119
147
|
@confirm_action = :retry_all unless @failed_jobs.empty?
|
|
120
148
|
nil
|
|
121
149
|
in { type: :key, code: "/" }
|
|
122
|
-
|
|
123
|
-
@filter_input = @filter || ""
|
|
150
|
+
enter_filter_mode
|
|
124
151
|
nil
|
|
125
152
|
in { type: :key, code: "esc" }
|
|
126
|
-
|
|
127
|
-
@filter_input = ""
|
|
128
|
-
:refresh
|
|
153
|
+
clear_filter
|
|
129
154
|
else
|
|
130
155
|
nil
|
|
131
156
|
end
|
|
@@ -148,7 +173,8 @@ module SolidQueueTui
|
|
|
148
173
|
Actions::DiscardJob.call(item.id)
|
|
149
174
|
:refresh
|
|
150
175
|
when :retry_all
|
|
151
|
-
|
|
176
|
+
f = filters
|
|
177
|
+
Actions::RetryJob.retry_all(filter: f[:class_name], queue: f[:queue])
|
|
152
178
|
:refresh
|
|
153
179
|
end
|
|
154
180
|
in { type: :key, code: "n" } | { type: :key, code: "esc" }
|
|
@@ -159,32 +185,11 @@ module SolidQueueTui
|
|
|
159
185
|
end
|
|
160
186
|
end
|
|
161
187
|
|
|
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)
|
|
169
|
-
: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
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
188
|
def move_selection(delta)
|
|
185
189
|
return if @failed_jobs.empty?
|
|
186
190
|
@selected_row = (@selected_row + delta).clamp(0, @failed_jobs.size - 1)
|
|
187
191
|
@table_state.select(@selected_row)
|
|
192
|
+
:load_more if needs_more?
|
|
188
193
|
end
|
|
189
194
|
|
|
190
195
|
def jump_to_top
|
|
@@ -196,6 +201,7 @@ module SolidQueueTui
|
|
|
196
201
|
return if @failed_jobs.empty?
|
|
197
202
|
@selected_row = @failed_jobs.size - 1
|
|
198
203
|
@table_state.select(@selected_row)
|
|
204
|
+
return :load_more if needs_more?
|
|
199
205
|
end
|
|
200
206
|
|
|
201
207
|
def render_failed_table(frame, area)
|
|
@@ -219,14 +225,13 @@ module SolidQueueTui
|
|
|
219
225
|
}
|
|
220
226
|
end
|
|
221
227
|
|
|
222
|
-
title = @filter ? "Failed Jobs (filter: #{@filter})" : "Failed Jobs"
|
|
223
|
-
|
|
224
228
|
table = Components::JobTable.new(
|
|
225
229
|
@tui,
|
|
226
|
-
title:
|
|
230
|
+
title: filter_title("Failed Jobs"),
|
|
227
231
|
columns: columns,
|
|
228
232
|
rows: rows,
|
|
229
233
|
selected_row: @selected_row,
|
|
234
|
+
total_count: @total_count,
|
|
230
235
|
empty_message: "No failed jobs — everything is running smoothly!"
|
|
231
236
|
)
|
|
232
237
|
|
|
@@ -242,7 +247,9 @@ module SolidQueueTui
|
|
|
242
247
|
job = selected_item
|
|
243
248
|
"Discard job ##{job&.job_id} (#{job&.class_name})? This cannot be undone. [y/n]"
|
|
244
249
|
when :retry_all
|
|
245
|
-
|
|
250
|
+
count = @total_count || @failed_jobs.size
|
|
251
|
+
label = @filters.empty? ? "failed jobs" : "filtered failed jobs"
|
|
252
|
+
"Retry ALL #{count} #{label}? [y/n]"
|
|
246
253
|
end
|
|
247
254
|
|
|
248
255
|
frame.render_widget(
|
|
@@ -261,23 +268,6 @@ module SolidQueueTui
|
|
|
261
268
|
)
|
|
262
269
|
end
|
|
263
270
|
|
|
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
271
|
def truncate(str, max)
|
|
282
272
|
return "" unless str
|
|
283
273
|
str.length > max ? "#{str[0...max - 3]}..." : str
|