solid_queue_tui 0.1.2 → 0.2.0
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/qtop +35 -0
- data/exe/sqtui +19 -2
- data/lib/solid_queue_tui/application.rb +37 -4
- data/lib/solid_queue_tui/cli.rb +2 -2
- data/lib/solid_queue_tui/components/job_table.rb +2 -3
- data/lib/solid_queue_tui/data/hourly_stats_query.rb +85 -0
- 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/data/stats.rb +15 -3
- 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 +9 -77
- data/lib/solid_queue_tui/views/concerns/confirmable.rb +53 -0
- data/lib/solid_queue_tui/views/concerns/filterable.rb +4 -0
- data/lib/solid_queue_tui/views/concerns/paginatable.rb +79 -0
- data/lib/solid_queue_tui/views/dashboard_view.rb +228 -22
- data/lib/solid_queue_tui/views/failed_view.rb +49 -152
- data/lib/solid_queue_tui/views/finished_view.rb +12 -80
- data/lib/solid_queue_tui/views/in_progress_view.rb +9 -72
- 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 +226 -87
- data/lib/solid_queue_tui/views/recurring_tasks_view.rb +22 -69
- data/lib/solid_queue_tui/views/scheduled_view.rb +39 -142
- data/lib/solid_queue_tui.rb +4 -0
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edcec2bda0cc128879fda2b066fcbd560c7879c4a2554d53d3ff01c50c4d5040
|
|
4
|
+
data.tar.gz: 2457d9eb670fddb7a851f64dbe37c1f2b69fdcd9608d69e2feb5b0af4b406358
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6f0174997d605c9e0d83c22c8a53973b017cd2e4cdd9cc260ace6f249d1a029c0c475a78124d5fd9537b9e5d1ca361c4b64adbd4a47c5842d0d05ede3ae153d
|
|
7
|
+
data.tar.gz: af07fa966c63b5f63b4f2e8d034e0d44b023294d443403b9130a631a5ecd3c52913dc0d71ef42ae85de8d81d37fc9e3c5e8def0f983cff6e372526bdf8e710bd
|
data/exe/qtop
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Boot the host Rails application's environment.
|
|
5
|
+
# This ensures ActiveRecord, Solid Queue models, and the database
|
|
6
|
+
# connection pool are fully loaded before the TUI starts.
|
|
7
|
+
env_file = File.join(Dir.pwd, "config", "environment.rb")
|
|
8
|
+
|
|
9
|
+
unless File.exist?(env_file)
|
|
10
|
+
$stderr.puts "Error: config/environment.rb not found in #{Dir.pwd}"
|
|
11
|
+
$stderr.puts ""
|
|
12
|
+
$stderr.puts "qtop must be run from your Rails application's root directory."
|
|
13
|
+
$stderr.puts "Make sure solid_queue_tui is in your Gemfile and Solid Queue is configured."
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require env_file
|
|
18
|
+
|
|
19
|
+
# Logs must never hit STDOUT/STDERR — that would corrupt the TUI.
|
|
20
|
+
# If a log/ directory exists (traditional Rails), write there.
|
|
21
|
+
# Otherwise (Rails 8 defaults), silence logging.
|
|
22
|
+
#TODO: figure out logging for rails 8, docker
|
|
23
|
+
|
|
24
|
+
log_dir = File.join(Dir.pwd, "log")
|
|
25
|
+
if Dir.exist?(log_dir)
|
|
26
|
+
log_file = File.join(log_dir, "#{Rails.env}.log")
|
|
27
|
+
tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(log_file))
|
|
28
|
+
tui_logger.push_tags("SQTUI")
|
|
29
|
+
else
|
|
30
|
+
tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(File::NULL))
|
|
31
|
+
end
|
|
32
|
+
ActiveRecord::Base.logger = tui_logger
|
|
33
|
+
Rails.logger = tui_logger
|
|
34
|
+
|
|
35
|
+
SolidQueueTui::CLI.run(ARGV)
|
data/exe/sqtui
CHANGED
|
@@ -19,7 +19,24 @@ require env_file
|
|
|
19
19
|
# Log ActiveRecord queries to the Rails log file so TUI operations
|
|
20
20
|
# are visible in log/development.log (or whichever environment is active).
|
|
21
21
|
# Tag all TUI logs with [SQTUI] so they're easy to distinguish from app logs.
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
# CORRUPTING TERMINAL
|
|
23
|
+
# ActiveRecord::Base.logger = Rails.logger
|
|
24
|
+
# Rails.logger.push_tags("SQTUI") if Rails.logger.respond_to?(:push_tags)
|
|
25
|
+
|
|
26
|
+
# Logs must never hit STDOUT/STDERR — that would corrupt the TUI.
|
|
27
|
+
# If a log/ directory exists (traditional Rails), write there.
|
|
28
|
+
# Otherwise (Rails 8 defaults), silence logging.
|
|
29
|
+
#TODO: figure out logging for rails 8, docker
|
|
30
|
+
|
|
31
|
+
log_dir = File.join(Dir.pwd, "log")
|
|
32
|
+
if Dir.exist?(log_dir)
|
|
33
|
+
log_file = File.join(log_dir, "#{Rails.env}.log")
|
|
34
|
+
tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(log_file))
|
|
35
|
+
tui_logger.push_tags("SQTUI")
|
|
36
|
+
else
|
|
37
|
+
tui_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(File::NULL))
|
|
38
|
+
end
|
|
39
|
+
ActiveRecord::Base.logger = tui_logger
|
|
40
|
+
Rails.logger = tui_logger
|
|
24
41
|
|
|
25
42
|
SolidQueueTui::CLI.run(ARGV)
|
|
@@ -143,10 +143,17 @@ module SolidQueueTui
|
|
|
143
143
|
return false
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
# If view is in a modal state (filter, confirm), it gets all input
|
|
146
|
+
# If view is in a modal state (filter, confirm, detail sub-view), it gets all input
|
|
147
147
|
if current_view.respond_to?(:capturing_input?) && current_view.capturing_input?
|
|
148
148
|
result = current_view.handle_input(event)
|
|
149
|
-
|
|
149
|
+
case result
|
|
150
|
+
when :refresh, :enter_queue, :exit_queue
|
|
151
|
+
refresh_data!
|
|
152
|
+
when :load_more
|
|
153
|
+
load_more_data!
|
|
154
|
+
when :open_detail
|
|
155
|
+
open_detail
|
|
156
|
+
end
|
|
150
157
|
return false
|
|
151
158
|
end
|
|
152
159
|
|
|
@@ -241,11 +248,21 @@ module SolidQueueTui
|
|
|
241
248
|
return unless item
|
|
242
249
|
|
|
243
250
|
case @current_view
|
|
251
|
+
when VIEW_QUEUES
|
|
252
|
+
if current_view.detail_mode?
|
|
253
|
+
@job_detail.show(job: item) if item.respond_to?(:id)
|
|
254
|
+
else
|
|
255
|
+
result = current_view.handle_input({ type: :key, code: "enter" })
|
|
256
|
+
refresh_data! if result == :enter_queue
|
|
257
|
+
end
|
|
244
258
|
when VIEW_FAILED
|
|
245
259
|
failed_job = Data::FailedQuery.fetch_one(item.id) if item.respond_to?(:id)
|
|
246
260
|
@job_detail.show(failed_job: failed_job || item)
|
|
247
261
|
when VIEW_IN_PROGRESS, VIEW_BLOCKED, VIEW_SCHEDULED, VIEW_FINISHED
|
|
248
262
|
@job_detail.show(job: item) if item.respond_to?(:id)
|
|
263
|
+
when VIEW_WORKERS
|
|
264
|
+
running_jobs = Data::ProcessesQuery.fetch_running_jobs(process_id: item.id)
|
|
265
|
+
@job_detail.show(process: item, running_jobs: running_jobs)
|
|
249
266
|
end
|
|
250
267
|
end
|
|
251
268
|
|
|
@@ -262,8 +279,16 @@ module SolidQueueTui
|
|
|
262
279
|
@stats = Data::Stats.fetch
|
|
263
280
|
current_view.update(stats: @stats)
|
|
264
281
|
when VIEW_QUEUES
|
|
265
|
-
|
|
266
|
-
|
|
282
|
+
if current_view.detail_mode?
|
|
283
|
+
q = current_view.selected_queue_name
|
|
284
|
+
f = current_view.filters
|
|
285
|
+
current_view.total_count = Data::JobsQuery.count_pending(queue: q, filter: f[:class_name])
|
|
286
|
+
jobs = Data::JobsQuery.fetch_pending(queue: q, filter: f[:class_name], limit: SolidQueueTui.page_size, offset: 0)
|
|
287
|
+
current_view.update_detail(jobs: jobs)
|
|
288
|
+
else
|
|
289
|
+
queues = Data::QueuesQuery.fetch
|
|
290
|
+
current_view.update(queues: queues)
|
|
291
|
+
end
|
|
267
292
|
when VIEW_FAILED
|
|
268
293
|
f = current_view.filters
|
|
269
294
|
current_view.total_count = Data::FailedQuery.count(filter: f[:class_name], queue: f[:queue])
|
|
@@ -305,6 +330,13 @@ module SolidQueueTui
|
|
|
305
330
|
offset = view.current_offset
|
|
306
331
|
|
|
307
332
|
case @current_view
|
|
333
|
+
when VIEW_QUEUES
|
|
334
|
+
if view.detail_mode?
|
|
335
|
+
q = view.selected_queue_name
|
|
336
|
+
f = view.filters
|
|
337
|
+
more = Data::JobsQuery.fetch_pending(queue: q, filter: f[:class_name], limit: SolidQueueTui.page_size, offset: offset)
|
|
338
|
+
view.append(jobs: more)
|
|
339
|
+
end
|
|
308
340
|
when VIEW_FAILED
|
|
309
341
|
f = view.filters
|
|
310
342
|
more = Data::FailedQuery.fetch(filter: f[:class_name], queue: f[:queue], limit: SolidQueueTui.page_size, offset: offset)
|
|
@@ -460,6 +492,7 @@ module SolidQueueTui
|
|
|
460
492
|
help_section("Actions"),
|
|
461
493
|
help_line("r", "Refresh data"),
|
|
462
494
|
help_line("/", "Filter by class name"),
|
|
495
|
+
help_line("c", "Clear active filter"),
|
|
463
496
|
help_line("R", "Retry failed job (in Failed view)"),
|
|
464
497
|
help_line("D", "Discard failed job (in Failed view)"),
|
|
465
498
|
help_line("A", "Retry all failed jobs"),
|
data/lib/solid_queue_tui/cli.rb
CHANGED
|
@@ -15,7 +15,7 @@ module SolidQueueTui
|
|
|
15
15
|
options = {}
|
|
16
16
|
|
|
17
17
|
OptionParser.new do |opts|
|
|
18
|
-
opts.banner = "Usage:
|
|
18
|
+
opts.banner = "Usage: qtop [options]"
|
|
19
19
|
opts.separator ""
|
|
20
20
|
opts.separator "Options:"
|
|
21
21
|
|
|
@@ -36,7 +36,7 @@ module SolidQueueTui
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
opts.on("-v", "--version", "Show version") do
|
|
39
|
-
puts "
|
|
39
|
+
puts "qtop v#{SolidQueueTui::VERSION}"
|
|
40
40
|
exit
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module SolidQueueTui
|
|
4
4
|
module Components
|
|
5
5
|
class JobTable
|
|
6
|
+
include FormattingHelpers
|
|
7
|
+
|
|
6
8
|
STATUS_COLORS = {
|
|
7
9
|
"ready" => :green,
|
|
8
10
|
"claimed" => :yellow,
|
|
@@ -49,9 +51,6 @@ module SolidQueueTui
|
|
|
49
51
|
text + " "
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
def format_number(n)
|
|
53
|
-
n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
54
|
-
end
|
|
55
54
|
|
|
56
55
|
def render_table(frame, area, table_state)
|
|
57
56
|
widths = @columns.map do |col|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Data
|
|
5
|
+
class HourlyStatsQuery
|
|
6
|
+
Result = Struct.new(:data, :total, :peak, :avg, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
def self.enqueued_per_hour
|
|
9
|
+
raw = SolidQueue::Job
|
|
10
|
+
.where(created_at: 24.hours.ago..)
|
|
11
|
+
.group(hour_sql(:created_at))
|
|
12
|
+
.count
|
|
13
|
+
build_result(raw)
|
|
14
|
+
rescue => e
|
|
15
|
+
empty_result
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.processed_per_hour
|
|
19
|
+
raw = SolidQueue::Job
|
|
20
|
+
.where.not(finished_at: nil)
|
|
21
|
+
.where(finished_at: 24.hours.ago..)
|
|
22
|
+
.group(hour_sql(:finished_at))
|
|
23
|
+
.count
|
|
24
|
+
build_result(raw)
|
|
25
|
+
rescue => e
|
|
26
|
+
empty_result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.failed_per_hour
|
|
30
|
+
raw = SolidQueue::FailedExecution
|
|
31
|
+
.where(created_at: 24.hours.ago..)
|
|
32
|
+
.group(hour_sql(:created_at))
|
|
33
|
+
.count
|
|
34
|
+
build_result(raw)
|
|
35
|
+
rescue => e
|
|
36
|
+
empty_result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.empty_result
|
|
40
|
+
now = Time.now.utc
|
|
41
|
+
data = (0..23).map { |i| (now - (23 - i) * 3600).strftime("%H").to_i }
|
|
42
|
+
Result.new(data: data.map { 0 }, total: 0, peak: 0, avg: 0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def hour_sql(column)
|
|
49
|
+
if sqlite?
|
|
50
|
+
Arel.sql("strftime('%Y-%m-%d %H:00:00', #{column})")
|
|
51
|
+
else
|
|
52
|
+
Arel.sql("DATE_TRUNC('hour', #{column})")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_result(raw_hash)
|
|
57
|
+
now = Time.now.utc
|
|
58
|
+
|
|
59
|
+
lookup = {}
|
|
60
|
+
raw_hash.each do |key, count|
|
|
61
|
+
time = key.is_a?(String) ? Time.parse("#{key} UTC") : key
|
|
62
|
+
lookup[time.strftime("%Y-%m-%d %H")] = count
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 24-slot array, oldest to newest — values only (for sparkline)
|
|
66
|
+
data = (0..23).map do |i|
|
|
67
|
+
hour_time = now - (23 - i) * 3600
|
|
68
|
+
key = hour_time.strftime("%Y-%m-%d %H")
|
|
69
|
+
lookup[key] || 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
total = data.sum
|
|
73
|
+
peak = data.max || 0
|
|
74
|
+
avg = total / 24
|
|
75
|
+
|
|
76
|
+
Result.new(data: data, total: total, peak: peak, avg: avg)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sqlite?
|
|
80
|
+
SolidQueue::Job.connection.adapter_name.downcase.include?("sqlite")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -13,6 +13,7 @@ module SolidQueueTui
|
|
|
13
13
|
|
|
14
14
|
def self.fetch(status:, filter: nil, queue: nil, limit: 100, offset: 0)
|
|
15
15
|
case status
|
|
16
|
+
when "pending" then fetch_pending(queue: queue, filter: filter, limit: limit, offset: offset)
|
|
16
17
|
when "claimed" then fetch_claimed(filter: filter, queue: queue, limit: limit, offset: offset)
|
|
17
18
|
when "blocked" then fetch_blocked(filter: filter, queue: queue, limit: limit, offset: offset)
|
|
18
19
|
when "scheduled" then fetch_scheduled(filter: filter, queue: queue, limit: limit, offset: offset)
|
|
@@ -25,6 +26,7 @@ module SolidQueueTui
|
|
|
25
26
|
|
|
26
27
|
def self.count(status:, filter: nil, queue: nil)
|
|
27
28
|
case status
|
|
29
|
+
when "pending" then count_pending(queue: queue, filter: filter)
|
|
28
30
|
when "claimed" then count_scope(SolidQueue::ClaimedExecution.joins(:job), filter: filter, queue: queue)
|
|
29
31
|
when "blocked" then count_scope(SolidQueue::BlockedExecution.joins(:job), filter: filter, queue: queue)
|
|
30
32
|
when "scheduled" then count_scope(SolidQueue::ScheduledExecution.joins(:job), filter: filter, queue: queue)
|
|
@@ -35,6 +37,35 @@ module SolidQueueTui
|
|
|
35
37
|
0
|
|
36
38
|
end
|
|
37
39
|
|
|
40
|
+
def self.fetch_pending(queue:, filter: nil, limit: 100, offset: 0)
|
|
41
|
+
scope = SolidQueue::ReadyExecution.joins(:job)
|
|
42
|
+
scope = scope.where(queue_name: queue) if queue
|
|
43
|
+
scope = apply_class_name_filter(scope, filter)
|
|
44
|
+
scope = scope.order(priority: :asc, job_id: :asc).offset(offset).limit(limit)
|
|
45
|
+
|
|
46
|
+
scope.includes(:job).map do |re|
|
|
47
|
+
job = re.job
|
|
48
|
+
Job.new(
|
|
49
|
+
id: job.id,
|
|
50
|
+
queue_name: job.queue_name,
|
|
51
|
+
class_name: job.class_name,
|
|
52
|
+
priority: job.priority,
|
|
53
|
+
status: "pending",
|
|
54
|
+
active_job_id: job.active_job_id,
|
|
55
|
+
arguments: job.arguments,
|
|
56
|
+
scheduled_at: job.scheduled_at,
|
|
57
|
+
created_at: job.created_at
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.count_pending(queue:, filter: nil)
|
|
63
|
+
scope = SolidQueue::ReadyExecution.joins(:job)
|
|
64
|
+
scope = scope.where(queue_name: queue) if queue
|
|
65
|
+
scope = apply_class_name_filter(scope, filter)
|
|
66
|
+
scope.count
|
|
67
|
+
end
|
|
68
|
+
|
|
38
69
|
def self.fetch_claimed(filter: nil, queue: nil, limit: 100, offset: 0)
|
|
39
70
|
scope = SolidQueue::ClaimedExecution.joins(:job)
|
|
40
71
|
scope = scope.merge(SolidQueue::Job.where(queue_name: queue)) if queue
|
|
@@ -29,6 +29,29 @@ module SolidQueueTui
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
RunningJob = Struct.new(
|
|
33
|
+
:job_id, :class_name, :queue_name, :started_at,
|
|
34
|
+
keyword_init: true
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def self.fetch_running_jobs(process_id:)
|
|
38
|
+
SolidQueue::ClaimedExecution
|
|
39
|
+
.where(process_id: process_id)
|
|
40
|
+
.joins(:job).includes(:job)
|
|
41
|
+
.order(:created_at)
|
|
42
|
+
.map do |ce|
|
|
43
|
+
job = ce.job
|
|
44
|
+
RunningJob.new(
|
|
45
|
+
job_id: job.id,
|
|
46
|
+
class_name: job.class_name,
|
|
47
|
+
queue_name: job.queue_name,
|
|
48
|
+
started_at: ce.created_at
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
rescue => e
|
|
52
|
+
[]
|
|
53
|
+
end
|
|
54
|
+
|
|
32
55
|
def self.fetch
|
|
33
56
|
SolidQueue::Process.where(kind: "Worker").order(:id).map do |proc|
|
|
34
57
|
Process.new(
|
|
@@ -5,7 +5,9 @@ module SolidQueueTui
|
|
|
5
5
|
class Stats
|
|
6
6
|
attr_reader :ready, :claimed, :failed, :scheduled, :blocked,
|
|
7
7
|
:total_jobs, :completed_jobs, :process_count,
|
|
8
|
-
:processes_by_kind
|
|
8
|
+
:processes_by_kind,
|
|
9
|
+
:enqueued_per_hour, :processed_per_hour, :failed_per_hour,
|
|
10
|
+
:queue_depths
|
|
9
11
|
|
|
10
12
|
def initialize(data)
|
|
11
13
|
@ready = data[:ready]
|
|
@@ -17,6 +19,10 @@ module SolidQueueTui
|
|
|
17
19
|
@completed_jobs = data[:completed_jobs]
|
|
18
20
|
@process_count = data[:process_count]
|
|
19
21
|
@processes_by_kind = data[:processes_by_kind]
|
|
22
|
+
@enqueued_per_hour = data[:enqueued_per_hour]
|
|
23
|
+
@processed_per_hour = data[:processed_per_hour]
|
|
24
|
+
@failed_per_hour = data[:failed_per_hour]
|
|
25
|
+
@queue_depths = data[:queue_depths]
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
def self.fetch
|
|
@@ -29,7 +35,11 @@ module SolidQueueTui
|
|
|
29
35
|
total_jobs: SolidQueue::Job.count,
|
|
30
36
|
completed_jobs: SolidQueue::Job.finished.count,
|
|
31
37
|
process_count: SolidQueue::Process.count,
|
|
32
|
-
processes_by_kind: SolidQueue::Process.group(:kind).count
|
|
38
|
+
processes_by_kind: SolidQueue::Process.group(:kind).count,
|
|
39
|
+
enqueued_per_hour: HourlyStatsQuery.enqueued_per_hour,
|
|
40
|
+
processed_per_hour: HourlyStatsQuery.processed_per_hour,
|
|
41
|
+
failed_per_hour: HourlyStatsQuery.failed_per_hour,
|
|
42
|
+
queue_depths: SolidQueue::ReadyExecution.group(:queue_name).count
|
|
33
43
|
)
|
|
34
44
|
rescue => e
|
|
35
45
|
empty(error: e.message)
|
|
@@ -39,7 +49,9 @@ module SolidQueueTui
|
|
|
39
49
|
new(
|
|
40
50
|
ready: 0, claimed: 0, failed: 0, scheduled: 0, blocked: 0,
|
|
41
51
|
total_jobs: 0, completed_jobs: 0, process_count: 0,
|
|
42
|
-
processes_by_kind: {}
|
|
52
|
+
processes_by_kind: {},
|
|
53
|
+
enqueued_per_hour: nil, processed_per_hour: nil, failed_per_hour: nil,
|
|
54
|
+
queue_depths: {}
|
|
43
55
|
)
|
|
44
56
|
end
|
|
45
57
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module FormattingHelpers
|
|
5
|
+
def time_ago(time)
|
|
6
|
+
return "n/a" unless time
|
|
7
|
+
seconds = (Time.now.utc - time).to_i
|
|
8
|
+
case seconds
|
|
9
|
+
when 0..59 then "#{seconds}s ago"
|
|
10
|
+
when 60..3599 then "#{seconds / 60}m ago"
|
|
11
|
+
when 3600..86399 then "#{seconds / 3600}h ago"
|
|
12
|
+
else "#{seconds / 86400}d ago"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def format_time(time)
|
|
17
|
+
return "n/a" unless time
|
|
18
|
+
time.strftime("%Y-%m-%d %H:%M:%S")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def format_duration(seconds)
|
|
22
|
+
return "n/a" unless seconds
|
|
23
|
+
seconds = seconds.to_i
|
|
24
|
+
if seconds < 1
|
|
25
|
+
"<1s"
|
|
26
|
+
elsif seconds < 60
|
|
27
|
+
"#{seconds}s"
|
|
28
|
+
elsif seconds < 3600
|
|
29
|
+
"#{seconds / 60}m #{seconds % 60}s"
|
|
30
|
+
elsif seconds < 86400
|
|
31
|
+
"#{seconds / 3600}h #{(seconds % 3600) / 60}m"
|
|
32
|
+
else
|
|
33
|
+
"#{seconds / 86400}d #{(seconds % 86400) / 3600}h"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def format_number(n)
|
|
38
|
+
return "0" if n.nil? || n == 0
|
|
39
|
+
n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def truncate(str, max)
|
|
43
|
+
return "" unless str
|
|
44
|
+
str.length > max ? "#{str[0...max - 3]}..." : str
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def humanize_duration(seconds)
|
|
48
|
+
case seconds.abs
|
|
49
|
+
when 0..59 then "#{seconds.abs}s"
|
|
50
|
+
when 60..3599 then "#{seconds.abs / 60}m"
|
|
51
|
+
when 3600..86399 then "#{seconds.abs / 3600}h"
|
|
52
|
+
else "#{seconds.abs / 86400}d"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def time_until(time)
|
|
57
|
+
return "n/a" unless time
|
|
58
|
+
seconds = (time - Time.now.utc).to_i
|
|
59
|
+
return "now" if seconds <= 0
|
|
60
|
+
"in #{humanize_duration(seconds)}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -4,47 +4,21 @@ module SolidQueueTui
|
|
|
4
4
|
module Views
|
|
5
5
|
class BlockedView
|
|
6
6
|
include Filterable
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
LOAD_THRESHOLD = 10
|
|
7
|
+
include Paginatable
|
|
8
|
+
include FormattingHelpers
|
|
10
9
|
|
|
11
10
|
def initialize(tui)
|
|
12
11
|
@tui = tui
|
|
13
|
-
|
|
14
|
-
@table_state.select(0)
|
|
15
|
-
@selected_row = 0
|
|
16
|
-
@jobs = []
|
|
17
|
-
@total_count = nil
|
|
18
|
-
@all_loaded = false
|
|
12
|
+
init_pagination
|
|
19
13
|
init_filter
|
|
20
14
|
end
|
|
21
15
|
|
|
22
16
|
def update(jobs:)
|
|
23
|
-
|
|
24
|
-
@all_loaded = jobs.size < SolidQueueTui.page_size
|
|
25
|
-
@selected_row = @selected_row.clamp(0, [@jobs.size - 1, 0].max)
|
|
26
|
-
@table_state.select(@selected_row)
|
|
17
|
+
update_items(jobs)
|
|
27
18
|
end
|
|
28
19
|
|
|
29
20
|
def append(jobs:)
|
|
30
|
-
|
|
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)
|
|
21
|
+
append_items(jobs)
|
|
48
22
|
end
|
|
49
23
|
|
|
50
24
|
def render(frame, area)
|
|
@@ -72,11 +46,6 @@ module SolidQueueTui
|
|
|
72
46
|
end
|
|
73
47
|
end
|
|
74
48
|
|
|
75
|
-
def selected_item
|
|
76
|
-
return nil if @jobs.empty? || @selected_row >= @jobs.size
|
|
77
|
-
@jobs[@selected_row]
|
|
78
|
-
end
|
|
79
|
-
|
|
80
49
|
def bindings
|
|
81
50
|
if filter_mode?
|
|
82
51
|
filter_bindings
|
|
@@ -85,8 +54,9 @@ module SolidQueueTui
|
|
|
85
54
|
{ key: "j/k", action: "Navigate" },
|
|
86
55
|
{ key: "Enter", action: "Detail" },
|
|
87
56
|
{ key: "/", action: "Filter" },
|
|
57
|
+
clear_filter_binding,
|
|
88
58
|
{ key: "G/g", action: "Bottom/Top" }
|
|
89
|
-
]
|
|
59
|
+
].compact
|
|
90
60
|
end
|
|
91
61
|
end
|
|
92
62
|
|
|
@@ -100,10 +70,6 @@ module SolidQueueTui
|
|
|
100
70
|
|
|
101
71
|
private
|
|
102
72
|
|
|
103
|
-
def needs_more?
|
|
104
|
-
!@all_loaded && @selected_row >= @jobs.size - LOAD_THRESHOLD
|
|
105
|
-
end
|
|
106
|
-
|
|
107
73
|
def handle_normal_input(event)
|
|
108
74
|
case event
|
|
109
75
|
in { type: :key, code: "j" } | { type: :key, code: "up" }
|
|
@@ -119,32 +85,13 @@ module SolidQueueTui
|
|
|
119
85
|
in { type: :key, code: "/" }
|
|
120
86
|
enter_filter_mode
|
|
121
87
|
nil
|
|
122
|
-
in { type: :key, code: "
|
|
88
|
+
in { type: :key, code: "c" }
|
|
123
89
|
clear_filter
|
|
124
90
|
else
|
|
125
91
|
nil
|
|
126
92
|
end
|
|
127
93
|
end
|
|
128
94
|
|
|
129
|
-
def move_selection(delta)
|
|
130
|
-
return if @jobs.empty?
|
|
131
|
-
@selected_row = (@selected_row + delta).clamp(0, @jobs.size - 1)
|
|
132
|
-
@table_state.select(@selected_row)
|
|
133
|
-
:load_more if needs_more?
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def jump_to_top
|
|
137
|
-
@selected_row = 0
|
|
138
|
-
@table_state.select(0)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def jump_to_bottom
|
|
142
|
-
return if @jobs.empty?
|
|
143
|
-
@selected_row = @jobs.size - 1
|
|
144
|
-
@table_state.select(@selected_row)
|
|
145
|
-
return :load_more if needs_more?
|
|
146
|
-
end
|
|
147
|
-
|
|
148
95
|
def render_table(frame, area)
|
|
149
96
|
columns = [
|
|
150
97
|
{ key: :id, label: "ID", width: 8 },
|
|
@@ -156,7 +103,7 @@ module SolidQueueTui
|
|
|
156
103
|
{ key: :blocked_since, label: "BLOCKED SINCE", width: 14 }
|
|
157
104
|
]
|
|
158
105
|
|
|
159
|
-
rows =
|
|
106
|
+
rows = items.map do |job|
|
|
160
107
|
{
|
|
161
108
|
id: job.id,
|
|
162
109
|
queue_name: job.queue_name,
|
|
@@ -181,21 +128,6 @@ module SolidQueueTui
|
|
|
181
128
|
table.render(frame, area, @table_state)
|
|
182
129
|
end
|
|
183
130
|
|
|
184
|
-
def format_time(time)
|
|
185
|
-
return "n/a" unless time
|
|
186
|
-
time.strftime("%Y-%m-%d %H:%M:%S")
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def time_ago(time)
|
|
190
|
-
return "n/a" unless time
|
|
191
|
-
seconds = (Time.now.utc - time).to_i
|
|
192
|
-
case seconds
|
|
193
|
-
when 0..59 then "#{seconds}s ago"
|
|
194
|
-
when 60..3599 then "#{seconds / 60}m ago"
|
|
195
|
-
when 3600..86399 then "#{seconds / 3600}h ago"
|
|
196
|
-
else "#{seconds / 86400}d ago"
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
131
|
end
|
|
200
132
|
end
|
|
201
133
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Views
|
|
5
|
+
module Confirmable
|
|
6
|
+
def init_confirm
|
|
7
|
+
@confirm_action = nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def confirm_mode? = !!@confirm_action
|
|
11
|
+
|
|
12
|
+
def confirm_bindings
|
|
13
|
+
[{ key: "y", action: "Confirm" }, { key: "n/Esc", action: "Cancel" }]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def handle_confirm_input(event)
|
|
17
|
+
case event
|
|
18
|
+
in { type: :key, code: "y" }
|
|
19
|
+
action = @confirm_action
|
|
20
|
+
@confirm_action = nil
|
|
21
|
+
execute_confirm_action(action)
|
|
22
|
+
in { type: :key, code: "n" } | { type: :key, code: "esc" }
|
|
23
|
+
@confirm_action = nil
|
|
24
|
+
nil
|
|
25
|
+
else
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render_confirm_popup(frame, area)
|
|
31
|
+
popup_area = area.centered(
|
|
32
|
+
@tui.constraint_percentage(50),
|
|
33
|
+
@tui.constraint_length(5)
|
|
34
|
+
)
|
|
35
|
+
frame.render_widget(@tui.clear(), popup_area)
|
|
36
|
+
frame.render_widget(
|
|
37
|
+
@tui.paragraph(
|
|
38
|
+
text: " #{confirm_message}",
|
|
39
|
+
style: @tui.style(fg: :yellow, modifiers: [:bold]),
|
|
40
|
+
block: @tui.block(
|
|
41
|
+
title: " Confirm ",
|
|
42
|
+
title_style: @tui.style(fg: :red, modifiers: [:bold]),
|
|
43
|
+
borders: [:all],
|
|
44
|
+
border_type: :rounded,
|
|
45
|
+
border_style: @tui.style(fg: :red)
|
|
46
|
+
)
|
|
47
|
+
),
|
|
48
|
+
popup_area
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|