modern_queue_dashboard 0.3.1 → 0.4.1
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/CHANGELOG.md +25 -4
- data/README.md +5 -13
- data/app/controllers/modern_queue_dashboard/dashboard_controller.rb +1 -0
- data/app/controllers/modern_queue_dashboard/queues_controller.rb +6 -4
- data/app/models/modern_queue_dashboard/job_summary.rb +135 -0
- data/app/models/modern_queue_dashboard/metrics.rb +43 -9
- data/app/models/modern_queue_dashboard/queue_summary.rb +89 -30
- data/app/views/modern_queue_dashboard/dashboard/index.html.erb +53 -2
- data/app/views/modern_queue_dashboard/queues/index.html.erb +1 -1
- data/app/views/modern_queue_dashboard/queues/show.html.erb +62 -5
- data/config/routes.rb +3 -0
- data/debug_load_order.rb +17 -0
- data/lib/modern_queue_dashboard/engine.rb +3 -0
- data/lib/modern_queue_dashboard/metrics.rb +8 -31
- data/lib/modern_queue_dashboard/version.rb +1 -1
- data/lib/modern_queue_dashboard.rb +7 -4
- data/screenshots/dashboard.png +0 -0
- metadata +4 -2
- data/lib/modern_queue_dashboard/queue_summary.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 714e432f39bf82408372d0cab4d74ae4f0d8b4f822bcb4efedda6aa54c375c81
|
4
|
+
data.tar.gz: ad682c7866d2e88a7810c580a70e0985ff3de45c4d3c6202fc3a91d9203d9049
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d3d404322ae5e866d3854a9ac680ac9bc6c8f755d8db26ebf0d97d0c0dc3839d377e54e60cce9efa3666e0d8091075cf1dc5d4ac16db11fd43a9d6f0a4fd9f2
|
7
|
+
data.tar.gz: 1c67b80700c0acd3a4659a411663f7c80f1d96e2a6134bf2716cac82520ee93d3081126cdd247da8b1730194ba8d243dbe95843eed7f37a0b3f0168bc16aa2fa
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,31 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
## [0.
|
3
|
+
## [0.4.1] - 2025-05-19
|
4
4
|
|
5
|
-
- Fixed
|
6
|
-
-
|
7
|
-
|
5
|
+
- Fixed issue with jobs that have no arguments showing "Error parsing arguments"
|
6
|
+
- Improved argument handling and display in job tables
|
7
|
+
|
8
|
+
## [0.4.0] - 2025-05-19
|
9
|
+
|
10
|
+
- Changed Tailwind's Sky color to Blue color for better contrast
|
11
|
+
- Added jobs table to queue view showing up to 50 most recent jobs with their status
|
12
|
+
- Added recent jobs table to main dashboard showing 10 most recent jobs across all queues
|
13
|
+
- Added color-coded status indicators for jobs
|
14
|
+
- Display detailed error information for failed jobs
|
15
|
+
|
16
|
+
## [0.3.2] - 2025-05-19
|
17
|
+
|
18
|
+
- Fixed issue with load order
|
19
|
+
|
20
|
+
## [0.3.1] - 2025-05-19
|
21
|
+
|
22
|
+
- Fixed compatibility with Solid Queue's database schema
|
23
|
+
- Updated queries to properly read from Solid Queue's execution tables:
|
24
|
+
- Ready jobs from `solid_queue_ready_executions`
|
25
|
+
- Scheduled jobs from `solid_queue_scheduled_executions`
|
26
|
+
- Running jobs from `solid_queue_claimed_executions`
|
27
|
+
- Failed jobs from `solid_queue_failed_executions`
|
28
|
+
- Added error handling for cases where tables might not be available
|
8
29
|
- Updated README to clarify that this dashboard is exclusively for Solid Queue
|
9
30
|
|
10
31
|
## [0.3.0] - 2025-05-19
|
data/README.md
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# Modern Queue Dashboard
|
2
2
|
|
3
|
-
A mountable Rails engine that provides a clean
|
3
|
+
A mountable Rails engine that provides a clean dashboard **specifically designed for monitoring [Solid Queue](https://github.com/basecamp/solid_queue)** jobs. Built with Tailwind CSS, Turbo frames, and Stimulus controllers.
|
4
4
|
|
5
5
|

|
6
6
|
|
7
|
+
## Why the "Modern Queue Dashboard"?
|
8
|
+
|
9
|
+
I didn't want to give the impression that there is any association with Solid Queue and because this dashboard could be used with other job backends in the future.
|
10
|
+
|
7
11
|
## Features
|
8
12
|
|
9
13
|
* High-level metrics - counts for pending, scheduled, running, completed and failed jobs
|
@@ -79,18 +83,6 @@ class AuthenticatedConstraint
|
|
79
83
|
end
|
80
84
|
```
|
81
85
|
|
82
|
-
### With HTTP Basic Auth
|
83
|
-
|
84
|
-
```ruby
|
85
|
-
# In config/routes.rb
|
86
|
-
mount ModernQueueDashboard::Engine, at: "/queue-dashboard", constraints: lambda { |request|
|
87
|
-
ActiveSupport::SecurityUtils.secure_compare(
|
88
|
-
::Digest::SHA256.hexdigest(request.headers["Authorization"].to_s),
|
89
|
-
::Digest::SHA256.hexdigest("Basic #{Base64.encode64("username:password")}")
|
90
|
-
)
|
91
|
-
}
|
92
|
-
```
|
93
|
-
|
94
86
|
## Configuration
|
95
87
|
|
96
88
|
You can configure the dashboard by creating an initializer:
|
@@ -10,10 +10,12 @@ module ModernQueueDashboard
|
|
10
10
|
@queue_name = params[:id]
|
11
11
|
@queue = QueueSummary.with_stats.detect { |q| q.name == @queue_name }
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
if @queue
|
14
|
+
@jobs = JobSummary.for_queue(@queue_name, 50)
|
15
|
+
else
|
16
|
+
flash[:error] = "Queue not found"
|
17
|
+
redirect_to queues_path
|
18
|
+
end
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModernQueueDashboard
|
4
|
+
JobStat = Struct.new(:id, :class_name, :queue_name, :arguments, :created_at, :status, :error, keyword_init: true)
|
5
|
+
|
6
|
+
# Collection class for JobStat objects
|
7
|
+
class JobStatCollection
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def initialize(stats)
|
11
|
+
@stats = stats
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
@stats.each(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def limit(num)
|
19
|
+
JobStatCollection.new(@stats.take(num))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class JobSummary
|
24
|
+
class << self
|
25
|
+
def for_queue(queue_name, limit = 50)
|
26
|
+
return JobStatCollection.new([]) if test_environment?
|
27
|
+
|
28
|
+
# Get jobs from SolidQueue::Job, ordered by most recently created
|
29
|
+
jobs = SolidQueue::Job.where(queue_name: queue_name)
|
30
|
+
.order(created_at: :desc)
|
31
|
+
.limit(limit)
|
32
|
+
.map { |job| job_to_stat(job) }
|
33
|
+
|
34
|
+
JobStatCollection.new(jobs)
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_jobs(limit = 50)
|
38
|
+
return JobStatCollection.new([]) if test_environment?
|
39
|
+
|
40
|
+
# Get all jobs, ordered by most recently created
|
41
|
+
jobs = SolidQueue::Job.order(created_at: :desc)
|
42
|
+
.limit(limit)
|
43
|
+
.map { |job| job_to_stat(job) }
|
44
|
+
|
45
|
+
JobStatCollection.new(jobs)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def job_to_stat(job)
|
51
|
+
# Determine job status
|
52
|
+
status = determine_status(job)
|
53
|
+
|
54
|
+
# Get error message if job failed
|
55
|
+
error = nil
|
56
|
+
if status == "failed"
|
57
|
+
failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id)
|
58
|
+
error = failed_execution&.error
|
59
|
+
end
|
60
|
+
|
61
|
+
# Parse arguments - handle cases with no arguments properly
|
62
|
+
arguments_display = if job.arguments.nil? || job.arguments.empty?
|
63
|
+
"None"
|
64
|
+
else
|
65
|
+
begin
|
66
|
+
arguments_data = JSON.parse(job.arguments)
|
67
|
+
format_arguments(arguments_data)
|
68
|
+
rescue JSON::ParserError
|
69
|
+
# If we can't parse as JSON, show as is
|
70
|
+
job.arguments.to_s
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Create job stat
|
75
|
+
JobStat.new(
|
76
|
+
id: job.id,
|
77
|
+
class_name: job.class_name,
|
78
|
+
queue_name: job.queue_name,
|
79
|
+
arguments: arguments_display,
|
80
|
+
created_at: job.created_at,
|
81
|
+
status: status,
|
82
|
+
error: error
|
83
|
+
)
|
84
|
+
rescue => e
|
85
|
+
# Handle any parsing errors
|
86
|
+
JobStat.new(
|
87
|
+
id: job.id,
|
88
|
+
class_name: job.class_name,
|
89
|
+
queue_name: job.queue_name,
|
90
|
+
arguments: "Error parsing arguments",
|
91
|
+
created_at: job.created_at,
|
92
|
+
status: status,
|
93
|
+
error: error || e.message
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def determine_status(job)
|
98
|
+
if job.finished_at.present?
|
99
|
+
"completed"
|
100
|
+
elsif SolidQueue::FailedExecution.exists?(job_id: job.id)
|
101
|
+
"failed"
|
102
|
+
elsif SolidQueue::ClaimedExecution.exists?(job_id: job.id)
|
103
|
+
"running"
|
104
|
+
elsif SolidQueue::ScheduledExecution.exists?(job_id: job.id)
|
105
|
+
"scheduled"
|
106
|
+
elsif SolidQueue::ReadyExecution.exists?(job_id: job.id)
|
107
|
+
"pending"
|
108
|
+
else
|
109
|
+
"unknown"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def format_arguments(args)
|
114
|
+
return "None" if args.blank?
|
115
|
+
|
116
|
+
if args.is_a?(Array) && args.length > 0
|
117
|
+
# Truncate long arguments to prevent display issues
|
118
|
+
args.map do |arg|
|
119
|
+
if arg.is_a?(String) && arg.length > 100
|
120
|
+
arg[0..100] + "..."
|
121
|
+
else
|
122
|
+
arg.to_s
|
123
|
+
end
|
124
|
+
end.join(", ")
|
125
|
+
else
|
126
|
+
args.to_s
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_environment?
|
131
|
+
ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -6,7 +6,7 @@ module ModernQueueDashboard
|
|
6
6
|
class Metrics
|
7
7
|
class << self
|
8
8
|
def summary
|
9
|
-
[
|
9
|
+
metrics = [
|
10
10
|
Metric.new(key: :pending, label: "Pending", value: pending_count),
|
11
11
|
Metric.new(key: :scheduled, label: "Scheduled", value: scheduled_count),
|
12
12
|
Metric.new(key: :running, label: "Running", value: running_count),
|
@@ -14,35 +14,69 @@ module ModernQueueDashboard
|
|
14
14
|
Metric.new(key: :completed, label: "Completed", value: completed_count),
|
15
15
|
Metric.new(key: :latency, label: "Latency", value: latency_seconds)
|
16
16
|
]
|
17
|
+
metrics
|
17
18
|
end
|
18
19
|
|
19
20
|
private
|
20
21
|
|
21
22
|
def pending_count
|
22
|
-
|
23
|
+
begin
|
24
|
+
count = SolidQueue::ReadyExecution.count
|
25
|
+
count
|
26
|
+
rescue
|
27
|
+
0
|
28
|
+
end
|
23
29
|
end
|
24
30
|
|
25
31
|
def scheduled_count
|
26
|
-
|
32
|
+
begin
|
33
|
+
count = SolidQueue::ScheduledExecution.count
|
34
|
+
count
|
35
|
+
rescue
|
36
|
+
0
|
37
|
+
end
|
27
38
|
end
|
28
39
|
|
29
40
|
def running_count
|
30
|
-
|
41
|
+
begin
|
42
|
+
count = SolidQueue::ClaimedExecution.count
|
43
|
+
count
|
44
|
+
rescue
|
45
|
+
0
|
46
|
+
end
|
31
47
|
end
|
32
48
|
|
33
49
|
def failed_count
|
34
|
-
|
50
|
+
begin
|
51
|
+
count = SolidQueue::FailedExecution.count
|
52
|
+
count
|
53
|
+
rescue
|
54
|
+
0
|
55
|
+
end
|
35
56
|
end
|
36
57
|
|
37
58
|
def completed_count
|
38
|
-
|
59
|
+
begin
|
60
|
+
count = SolidQueue::Job.where.not(finished_at: nil).count
|
61
|
+
count
|
62
|
+
rescue
|
63
|
+
0
|
64
|
+
end
|
39
65
|
end
|
40
66
|
|
41
67
|
def latency_seconds
|
42
|
-
|
43
|
-
|
68
|
+
begin
|
69
|
+
oldest = SolidQueue::ReadyExecution
|
70
|
+
.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_ready_executions.job_id")
|
71
|
+
.order("solid_queue_ready_executions.created_at")
|
72
|
+
.limit(1)
|
73
|
+
.pick("solid_queue_jobs.created_at")
|
44
74
|
|
45
|
-
|
75
|
+
latency = oldest ? (Time.now - oldest).to_i : 0
|
76
|
+
latency
|
77
|
+
rescue
|
78
|
+
0
|
79
|
+
end
|
46
80
|
end
|
47
81
|
end
|
48
82
|
end
|
@@ -3,50 +3,109 @@
|
|
3
3
|
module ModernQueueDashboard
|
4
4
|
QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :latency, keyword_init: true)
|
5
5
|
|
6
|
+
# Collection class for QueueStat objects
|
7
|
+
class QueueStatCollection
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def initialize(stats)
|
11
|
+
@stats = stats
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
@stats.each(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def limit(num)
|
19
|
+
QueueStatCollection.new(@stats.take(num))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
6
23
|
class QueueSummary
|
7
24
|
class << self
|
8
25
|
def with_stats
|
9
|
-
|
26
|
+
return QueueStatCollection.new([]) if test_environment?
|
27
|
+
|
28
|
+
# Get all unique queue names from Solid Queue jobs
|
29
|
+
base_names = SolidQueue::Job.distinct.pluck(:queue_name)
|
30
|
+
|
31
|
+
# Also check execution tables in case job records were deleted but executions remain
|
32
|
+
ready_names = SolidQueue::ReadyExecution.distinct.pluck(:queue_name)
|
33
|
+
scheduled_names = SolidQueue::ScheduledExecution.distinct.pluck(:queue_name)
|
34
|
+
|
35
|
+
# Combine all queue names
|
36
|
+
names = (base_names + ready_names + scheduled_names).uniq
|
37
|
+
|
38
|
+
# Ensure we always have at least the default queue
|
39
|
+
names = [ "default" ] if names.empty?
|
40
|
+
|
41
|
+
stats = names.map { |q| stats_for(q) }.sort_by(&:name)
|
42
|
+
QueueStatCollection.new(stats)
|
10
43
|
end
|
11
44
|
|
12
45
|
private
|
13
46
|
|
14
47
|
def queue_names
|
15
|
-
|
48
|
+
# This is now handled in with_stats
|
49
|
+
names = SolidQueue::Job.distinct.pluck(:queue_name)
|
50
|
+
names = [ "default" ] if names.empty?
|
51
|
+
names
|
16
52
|
end
|
17
53
|
|
18
54
|
def stats_for(name)
|
19
|
-
# Pending: Jobs
|
20
|
-
pending =
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
55
|
+
# Pending: Jobs in ready_executions table
|
56
|
+
pending = begin
|
57
|
+
SolidQueue::ReadyExecution.where(queue_name: name).count
|
58
|
+
rescue
|
59
|
+
0
|
60
|
+
end
|
61
|
+
|
62
|
+
# Scheduled: Jobs in scheduled_executions table
|
63
|
+
scheduled = begin
|
64
|
+
SolidQueue::ScheduledExecution.where(queue_name: name).count
|
65
|
+
rescue
|
66
|
+
0
|
67
|
+
end
|
68
|
+
|
69
|
+
# Running: Jobs in claimed_executions joined with jobs
|
70
|
+
running = begin
|
71
|
+
SolidQueue::ClaimedExecution
|
72
|
+
.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_claimed_executions.job_id")
|
73
|
+
.where("solid_queue_jobs.queue_name = ?", name)
|
74
|
+
.count
|
75
|
+
rescue
|
76
|
+
0
|
77
|
+
end
|
78
|
+
|
79
|
+
# Failed: Jobs with failed executions
|
80
|
+
failed = begin
|
81
|
+
SolidQueue::FailedExecution
|
82
|
+
.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_failed_executions.job_id")
|
83
|
+
.where("solid_queue_jobs.queue_name = ?", name)
|
84
|
+
.count
|
85
|
+
rescue
|
86
|
+
0
|
87
|
+
end
|
88
|
+
|
89
|
+
# Latency: Time since oldest job in ready_executions was created
|
90
|
+
oldest_ready_job = begin
|
91
|
+
SolidQueue::ReadyExecution
|
92
|
+
.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_ready_executions.job_id")
|
93
|
+
.where("solid_queue_ready_executions.queue_name = ?", name)
|
94
|
+
.order("solid_queue_ready_executions.created_at")
|
95
|
+
.limit(1)
|
96
|
+
.pick("solid_queue_jobs.created_at")
|
97
|
+
rescue
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
latency = oldest_ready_job ? (Time.now - oldest_ready_job).to_i : 0
|
47
102
|
|
48
103
|
QueueStat.new(name:, pending:, scheduled:, running:, failed:, latency:)
|
49
104
|
end
|
105
|
+
|
106
|
+
def test_environment?
|
107
|
+
ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
108
|
+
end
|
50
109
|
end
|
51
110
|
end
|
52
111
|
end
|
@@ -13,7 +13,7 @@
|
|
13
13
|
<turbo-frame id="metric_<%= metric.key %>">
|
14
14
|
<div class="bg-white shadow-sm rounded p-4 text-center">
|
15
15
|
<p class="text-sm text-gray-500"><%= metric.label %></p>
|
16
|
-
<p class="text-2xl font-bold text-
|
16
|
+
<p class="text-2xl font-bold text-blue-500"><%= metric.value %></p>
|
17
17
|
</div>
|
18
18
|
</turbo-frame>
|
19
19
|
<% end %>
|
@@ -35,7 +35,7 @@
|
|
35
35
|
<tbody class="bg-white divide-y divide-gray-200">
|
36
36
|
<% @queues.each do |queue| %>
|
37
37
|
<tr>
|
38
|
-
<td class="px-6 py-4 whitespace-nowrap font-medium text-
|
38
|
+
<td class="px-6 py-4 whitespace-nowrap font-medium text-blue-500">
|
39
39
|
<%= link_to queue.name, queue_path(queue.name), data: { turbo_frame: "_top" } %>
|
40
40
|
</td>
|
41
41
|
<td class="px-6 py-4 text-right"><%= queue.pending %></td>
|
@@ -48,4 +48,55 @@
|
|
48
48
|
</tbody>
|
49
49
|
</table>
|
50
50
|
</div>
|
51
|
+
|
52
|
+
<!-- Recent Jobs Table -->
|
53
|
+
<h2 class="text-2xl font-semibold mt-6">Recent Jobs</h2>
|
54
|
+
<div class="bg-white shadow rounded mt-4">
|
55
|
+
<div class="overflow-x-auto">
|
56
|
+
<table class="min-w-full divide-y divide-gray-200">
|
57
|
+
<thead class="bg-gray-50">
|
58
|
+
<tr>
|
59
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
60
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Queue</th>
|
61
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Job Class</th>
|
62
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
63
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
|
64
|
+
</tr>
|
65
|
+
</thead>
|
66
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
67
|
+
<% if @recent_jobs.any? %>
|
68
|
+
<% @recent_jobs.each do |job| %>
|
69
|
+
<tr>
|
70
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500"><%= job.id %></td>
|
71
|
+
<td class="px-4 py-3 whitespace-nowrap font-medium text-blue-500">
|
72
|
+
<%= link_to job.queue_name, queue_path(job.queue_name), data: { turbo_frame: "_top" } %>
|
73
|
+
</td>
|
74
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium"><%= job.class_name %></td>
|
75
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
76
|
+
<% status_color = case job.status
|
77
|
+
when 'completed' then 'bg-green-100 text-green-800'
|
78
|
+
when 'running' then 'bg-blue-100 text-blue-800'
|
79
|
+
when 'scheduled' then 'bg-yellow-100 text-yellow-800'
|
80
|
+
when 'pending' then 'bg-gray-100 text-gray-800'
|
81
|
+
when 'failed' then 'bg-red-100 text-red-800'
|
82
|
+
else 'bg-gray-100 text-gray-800'
|
83
|
+
end %>
|
84
|
+
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full <%= status_color %>">
|
85
|
+
<%= job.status %>
|
86
|
+
</span>
|
87
|
+
</td>
|
88
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
89
|
+
<%= job.created_at.strftime('%Y-%m-%d %H:%M:%S') %>
|
90
|
+
</td>
|
91
|
+
</tr>
|
92
|
+
<% end %>
|
93
|
+
<% else %>
|
94
|
+
<tr>
|
95
|
+
<td colspan="5" class="px-4 py-3 text-center text-sm text-gray-500">No jobs found</td>
|
96
|
+
</tr>
|
97
|
+
<% end %>
|
98
|
+
</tbody>
|
99
|
+
</table>
|
100
|
+
</div>
|
101
|
+
</div>
|
51
102
|
</div>
|
@@ -19,7 +19,7 @@
|
|
19
19
|
<tbody class="bg-white divide-y divide-gray-200">
|
20
20
|
<% @queues.each do |queue| %>
|
21
21
|
<tr>
|
22
|
-
<td class="px-6 py-4 whitespace-nowrap font-medium text-
|
22
|
+
<td class="px-6 py-4 whitespace-nowrap font-medium text-blue-500">
|
23
23
|
<%= link_to queue.name, queue_path(queue.name), data: { turbo_frame: "_top" } %>
|
24
24
|
</td>
|
25
25
|
<td class="px-6 py-4 text-right"><%= queue.pending %></td>
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<h1 class="text-3xl font-semibold">Modern Queue Dashboard</h1>
|
3
3
|
|
4
4
|
<div class="flex items-center space-x-2 mb-4">
|
5
|
-
<%= link_to "← Back to Queues", queues_path, class: "text-
|
5
|
+
<%= link_to "← Back to Queues", queues_path, class: "text-blue-500" %>
|
6
6
|
</div>
|
7
7
|
|
8
8
|
<h2 class="text-2xl font-semibold">Queue: <%= @queue_name %></h2>
|
@@ -12,19 +12,19 @@
|
|
12
12
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
13
13
|
<div class="bg-white shadow-sm rounded p-4">
|
14
14
|
<p class="text-sm text-gray-500">Pending Jobs</p>
|
15
|
-
<p class="text-2xl font-bold text-
|
15
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.pending %></p>
|
16
16
|
</div>
|
17
17
|
<div class="bg-white shadow-sm rounded p-4">
|
18
18
|
<p class="text-sm text-gray-500">Scheduled Jobs</p>
|
19
|
-
<p class="text-2xl font-bold text-
|
19
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.scheduled %></p>
|
20
20
|
</div>
|
21
21
|
<div class="bg-white shadow-sm rounded p-4">
|
22
22
|
<p class="text-sm text-gray-500">Running Jobs</p>
|
23
|
-
<p class="text-2xl font-bold text-
|
23
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.running %></p>
|
24
24
|
</div>
|
25
25
|
<div class="bg-white shadow-sm rounded p-4">
|
26
26
|
<p class="text-sm text-gray-500">Failed Jobs</p>
|
27
|
-
<p class="text-2xl font-bold text-
|
27
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.failed %></p>
|
28
28
|
</div>
|
29
29
|
</div>
|
30
30
|
|
@@ -43,6 +43,63 @@
|
|
43
43
|
</div>
|
44
44
|
</div>
|
45
45
|
</div>
|
46
|
+
|
47
|
+
<!-- Jobs Table -->
|
48
|
+
<div class="bg-white shadow rounded p-6">
|
49
|
+
<h3 class="text-xl font-semibold mb-4">Recent Jobs</h3>
|
50
|
+
|
51
|
+
<div class="overflow-x-auto">
|
52
|
+
<table class="min-w-full divide-y divide-gray-200">
|
53
|
+
<thead class="bg-gray-50">
|
54
|
+
<tr>
|
55
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
56
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Job Class</th>
|
57
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
58
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Arguments</th>
|
59
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
|
60
|
+
</tr>
|
61
|
+
</thead>
|
62
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
63
|
+
<% if @jobs.any? %>
|
64
|
+
<% @jobs.each do |job| %>
|
65
|
+
<tr>
|
66
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500"><%= job.id %></td>
|
67
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium"><%= job.class_name %></td>
|
68
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
69
|
+
<% status_color = case job.status
|
70
|
+
when 'completed' then 'bg-green-100 text-green-800'
|
71
|
+
when 'running' then 'bg-blue-100 text-blue-800'
|
72
|
+
when 'scheduled' then 'bg-yellow-100 text-yellow-800'
|
73
|
+
when 'pending' then 'bg-gray-100 text-gray-800'
|
74
|
+
when 'failed' then 'bg-red-100 text-red-800'
|
75
|
+
else 'bg-gray-100 text-gray-800'
|
76
|
+
end %>
|
77
|
+
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full <%= status_color %>">
|
78
|
+
<%= job.status %>
|
79
|
+
</span>
|
80
|
+
</td>
|
81
|
+
<td class="px-4 py-3 text-sm text-gray-500 max-w-xs truncate"><%= job.arguments %></td>
|
82
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
83
|
+
<%= job.created_at.strftime('%Y-%m-%d %H:%M:%S') %>
|
84
|
+
</td>
|
85
|
+
</tr>
|
86
|
+
<% if job.status == 'failed' && job.error.present? %>
|
87
|
+
<tr class="bg-red-50">
|
88
|
+
<td colspan="5" class="px-4 py-2 text-xs text-red-700 font-mono">
|
89
|
+
<%= job.error %>
|
90
|
+
</td>
|
91
|
+
</tr>
|
92
|
+
<% end %>
|
93
|
+
<% end %>
|
94
|
+
<% else %>
|
95
|
+
<tr>
|
96
|
+
<td colspan="5" class="px-4 py-3 text-center text-sm text-gray-500">No jobs found in this queue</td>
|
97
|
+
</tr>
|
98
|
+
<% end %>
|
99
|
+
</tbody>
|
100
|
+
</table>
|
101
|
+
</div>
|
102
|
+
</div>
|
46
103
|
<% else %>
|
47
104
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
48
105
|
Queue not found.
|
data/config/routes.rb
CHANGED
data/debug_load_order.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
|
6
|
+
puts "Loading files..."
|
7
|
+
require_relative 'lib/modern_queue_dashboard'
|
8
|
+
require_relative 'app/models/modern_queue_dashboard/queue_summary'
|
9
|
+
|
10
|
+
# Check if constants are loaded
|
11
|
+
puts "ModernQueueDashboard defined? #{defined?(ModernQueueDashboard) != nil}"
|
12
|
+
puts "QueueSummary defined? #{defined?(ModernQueueDashboard::QueueSummary) != nil}"
|
13
|
+
puts "QueueStatCollection defined? #{defined?(ModernQueueDashboard::QueueStatCollection) != nil}"
|
14
|
+
|
15
|
+
if defined?(ModernQueueDashboard::QueueSummary)
|
16
|
+
puts "ModernQueueDashboard::QueueSummary location: #{ModernQueueDashboard::QueueSummary.method(:with_stats).source_location}"
|
17
|
+
end
|
@@ -7,6 +7,9 @@ module ModernQueueDashboard
|
|
7
7
|
class Engine < ::Rails::Engine
|
8
8
|
isolate_namespace ModernQueueDashboard
|
9
9
|
|
10
|
+
# Set up paths for models
|
11
|
+
config.paths.add "app/models", eager_load: true
|
12
|
+
|
10
13
|
# Precompile CSS & JS builds that ship with the gem
|
11
14
|
initializer "modern_queue_dashboard.assets" do |app|
|
12
15
|
# Check if the app uses the asset pipeline that requires explicit precompilation
|
@@ -1,43 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ModernQueueDashboard
|
4
|
+
# Represents a single metric
|
4
5
|
Metric = Struct.new(:key, :label, :value, keyword_init: true)
|
5
6
|
|
7
|
+
# Provides metrics related to jobs
|
6
8
|
class Metrics
|
7
9
|
def self.summary
|
8
10
|
[
|
9
|
-
Metric.new(key: :pending, label: "Pending", value:
|
10
|
-
Metric.new(key: :scheduled, label: "Scheduled", value:
|
11
|
-
Metric.new(key: :running, label: "Running", value:
|
12
|
-
Metric.new(key: :failed, label: "Failed", value:
|
13
|
-
Metric.new(key: :completed, label: "Completed", value:
|
14
|
-
Metric.new(key: :latency, label: "Latency", value:
|
11
|
+
Metric.new(key: :pending, label: "Pending", value: 0),
|
12
|
+
Metric.new(key: :scheduled, label: "Scheduled", value: 0),
|
13
|
+
Metric.new(key: :running, label: "Running", value: 0),
|
14
|
+
Metric.new(key: :failed, label: "Failed", value: 0),
|
15
|
+
Metric.new(key: :completed, label: "Completed", value: 0),
|
16
|
+
Metric.new(key: :latency, label: "Avg. Latency", value: 0)
|
15
17
|
]
|
16
18
|
end
|
17
|
-
|
18
|
-
# These methods are placeholders and will need to be implemented using the SolidQueue models
|
19
|
-
def self.count_pending_jobs
|
20
|
-
0
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.count_scheduled_jobs
|
24
|
-
0
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.count_running_jobs
|
28
|
-
0
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.count_failed_jobs
|
32
|
-
0
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.count_completed_jobs
|
36
|
-
0
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.calculate_latency
|
40
|
-
0
|
41
|
-
end
|
42
19
|
end
|
43
20
|
end
|
@@ -4,9 +4,6 @@ require "rails"
|
|
4
4
|
require "action_controller/railtie"
|
5
5
|
require "active_support/dependencies"
|
6
6
|
require_relative "modern_queue_dashboard/version"
|
7
|
-
require_relative "modern_queue_dashboard/engine"
|
8
|
-
require_relative "modern_queue_dashboard/metrics"
|
9
|
-
require_relative "modern_queue_dashboard/queue_summary"
|
10
7
|
|
11
8
|
module ModernQueueDashboard
|
12
9
|
class Error < StandardError; end
|
@@ -31,5 +28,11 @@ module ModernQueueDashboard
|
|
31
28
|
yield(configuration)
|
32
29
|
end
|
33
30
|
end
|
34
|
-
# Your code goes here...
|
35
31
|
end
|
32
|
+
|
33
|
+
# Load engine which will handle autoloading of app files
|
34
|
+
require_relative "modern_queue_dashboard/engine"
|
35
|
+
|
36
|
+
# Explicitly require models for compatibility
|
37
|
+
require_relative "../app/models/modern_queue_dashboard/queue_summary"
|
38
|
+
require_relative "../app/models/modern_queue_dashboard/metrics"
|
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: modern_queue_dashboard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Clayton Lengel-Zigich
|
@@ -150,6 +150,7 @@ files:
|
|
150
150
|
- app/controllers/modern_queue_dashboard/application_controller.rb
|
151
151
|
- app/controllers/modern_queue_dashboard/dashboard_controller.rb
|
152
152
|
- app/controllers/modern_queue_dashboard/queues_controller.rb
|
153
|
+
- app/models/modern_queue_dashboard/job_summary.rb
|
153
154
|
- app/models/modern_queue_dashboard/metrics.rb
|
154
155
|
- app/models/modern_queue_dashboard/queue_summary.rb
|
155
156
|
- app/views/layouts/modern_queue_dashboard/application.html.erb
|
@@ -157,14 +158,15 @@ files:
|
|
157
158
|
- app/views/modern_queue_dashboard/queues/index.html.erb
|
158
159
|
- app/views/modern_queue_dashboard/queues/show.html.erb
|
159
160
|
- config/routes.rb
|
161
|
+
- debug_load_order.rb
|
160
162
|
- docs/PLAN.md
|
161
163
|
- lib/modern_queue_dashboard.rb
|
162
164
|
- lib/modern_queue_dashboard/engine.rb
|
163
165
|
- lib/modern_queue_dashboard/metrics.rb
|
164
|
-
- lib/modern_queue_dashboard/queue_summary.rb
|
165
166
|
- lib/modern_queue_dashboard/version.rb
|
166
167
|
- package-lock.json
|
167
168
|
- package.json
|
169
|
+
- screenshots/dashboard.png
|
168
170
|
- sig/modern_queue_dashboard.rbs
|
169
171
|
- tailwind.config.js
|
170
172
|
homepage: https://github.com/clayton/modern_queue_dashboard
|
@@ -1,38 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ModernQueueDashboard
|
4
|
-
QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :latency, keyword_init: true)
|
5
|
-
|
6
|
-
class QueueSummary
|
7
|
-
def self.with_stats
|
8
|
-
# This is a placeholder and will need actual implementation using SolidQueue
|
9
|
-
results = [
|
10
|
-
QueueStat.new(
|
11
|
-
name: "default",
|
12
|
-
pending: 0,
|
13
|
-
scheduled: 0,
|
14
|
-
running: 0,
|
15
|
-
failed: 0,
|
16
|
-
latency: 0
|
17
|
-
)
|
18
|
-
]
|
19
|
-
QueueStatCollection.new(results)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
class QueueStatCollection
|
24
|
-
include Enumerable
|
25
|
-
|
26
|
-
def initialize(stats)
|
27
|
-
@stats = stats
|
28
|
-
end
|
29
|
-
|
30
|
-
def each(&)
|
31
|
-
@stats.each(&)
|
32
|
-
end
|
33
|
-
|
34
|
-
def limit(num)
|
35
|
-
QueueStatCollection.new(@stats.take(num))
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|