modern_queue_dashboard 0.3.1 → 0.5.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/CHANGELOG.md +34 -4
- data/README.md +13 -13
- data/app/assets/builds/modern_queue_dashboard.css +1 -1
- data/app/controllers/modern_queue_dashboard/dashboard_controller.rb +1 -0
- data/app/controllers/modern_queue_dashboard/jobs_controller.rb +114 -0
- data/app/controllers/modern_queue_dashboard/queues_controller.rb +119 -3
- data/app/models/modern_queue_dashboard/job_summary.rb +272 -0
- data/app/models/modern_queue_dashboard/metrics.rb +43 -9
- data/app/models/modern_queue_dashboard/queue_summary.rb +102 -33
- data/app/views/modern_queue_dashboard/dashboard/index.html.erb +53 -2
- data/app/views/modern_queue_dashboard/jobs/show.html.erb +102 -0
- data/app/views/modern_queue_dashboard/queues/index.html.erb +1 -1
- data/app/views/modern_queue_dashboard/queues/show.html.erb +175 -14
- data/config/routes.rb +17 -1
- data/debug_load_order.rb +17 -0
- data/lib/modern_queue_dashboard/engine.rb +14 -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/package.json +1 -1
- data/screenshots/dashboard.png +0 -0
- metadata +20 -2
- data/lib/modern_queue_dashboard/queue_summary.rb +0 -38
@@ -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
|
@@ -1,51 +1,120 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ModernQueueDashboard
|
4
|
-
QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :latency, keyword_init: true)
|
4
|
+
QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :completed, :latency, keyword_init: true)
|
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
|
5
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
|
-
|
47
|
-
|
48
|
-
|
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
|
+
# Completed: Jobs in completed_executions table
|
90
|
+
completed = begin
|
91
|
+
SolidQueue::Job
|
92
|
+
.where(queue_name: name)
|
93
|
+
.where.not(finished_at: nil)
|
94
|
+
.count
|
95
|
+
rescue
|
96
|
+
0
|
97
|
+
end
|
98
|
+
|
99
|
+
# Latency: Time since oldest job in ready_executions was created
|
100
|
+
oldest_ready_job = begin
|
101
|
+
SolidQueue::ReadyExecution
|
102
|
+
.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_ready_executions.job_id")
|
103
|
+
.where("solid_queue_ready_executions.queue_name = ?", name)
|
104
|
+
.order("solid_queue_ready_executions.created_at")
|
105
|
+
.limit(1)
|
106
|
+
.pick("solid_queue_jobs.created_at")
|
107
|
+
rescue
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
latency = oldest_ready_job ? (Time.now - oldest_ready_job).to_i : 0
|
112
|
+
|
113
|
+
QueueStat.new(name:, pending:, scheduled:, running:, failed:, completed:, latency:)
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_environment?
|
117
|
+
ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
49
118
|
end
|
50
119
|
end
|
51
120
|
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>
|
@@ -0,0 +1,102 @@
|
|
1
|
+
<div class="container mx-auto p-6 space-y-6">
|
2
|
+
<h1 class="text-3xl font-semibold">Job Details</h1>
|
3
|
+
|
4
|
+
<div class="flex items-center space-x-2 mb-4">
|
5
|
+
<%= link_to "← Back to Queue", queue_path(@queue_name), class: "text-blue-500" %>
|
6
|
+
</div>
|
7
|
+
|
8
|
+
<% if @job_stat %>
|
9
|
+
<!-- Job Overview Card -->
|
10
|
+
<div class="bg-white shadow rounded-lg p-6">
|
11
|
+
<div class="border-b border-gray-200 pb-4 mb-4">
|
12
|
+
<h2 class="text-2xl font-semibold">Job #<%= @job_stat.id %></h2>
|
13
|
+
<p class="text-gray-600"><%= @job_stat.class_name %></p>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
17
|
+
<div>
|
18
|
+
<h3 class="text-lg font-medium mb-3">Job Information</h3>
|
19
|
+
|
20
|
+
<div class="space-y-3">
|
21
|
+
<div>
|
22
|
+
<p class="text-sm text-gray-500">Status</p>
|
23
|
+
<% status_style = case @job_stat.status
|
24
|
+
when 'completed' then 'bg-green-50 text-green-700 ring-1 ring-green-600/20 ring-inset'
|
25
|
+
when 'running' then 'bg-blue-50 text-blue-700 ring-1 ring-blue-700/10 ring-inset'
|
26
|
+
when 'scheduled' then 'bg-yellow-50 text-yellow-800 ring-1 ring-yellow-600/20 ring-inset'
|
27
|
+
when 'pending' then 'bg-gray-50 text-gray-600 ring-1 ring-gray-500/10 ring-inset'
|
28
|
+
when 'failed' then 'bg-red-50 text-red-700 ring-1 ring-red-600/10 ring-inset'
|
29
|
+
else 'bg-purple-50 text-purple-700 ring-1 ring-purple-700/10 ring-inset'
|
30
|
+
end %>
|
31
|
+
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium <%= status_style %>">
|
32
|
+
<%= @job_stat.status %>
|
33
|
+
</span>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div>
|
37
|
+
<p class="text-sm text-gray-500">Queue</p>
|
38
|
+
<p class="font-medium"><%= @job_stat.queue_name %></p>
|
39
|
+
</div>
|
40
|
+
|
41
|
+
<div>
|
42
|
+
<p class="text-sm text-gray-500">Created At</p>
|
43
|
+
<p class="font-medium"><%= @job_stat.created_at.strftime('%Y-%m-%d %H:%M:%S') %></p>
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<div>
|
49
|
+
<h3 class="text-lg font-medium mb-3">Arguments</h3>
|
50
|
+
<div class="bg-gray-50 p-4 rounded-md overflow-auto max-h-40">
|
51
|
+
<pre class="text-sm text-gray-700 font-mono"><%= @job_stat.arguments %></pre>
|
52
|
+
</div>
|
53
|
+
|
54
|
+
<div class="mt-6 flex space-x-3">
|
55
|
+
<% if ['failed', 'scheduled', 'pending', 'completed'].include?(@job_stat.status) %>
|
56
|
+
<%= button_to "Retry Job", retry_job_path(@job_stat.id),
|
57
|
+
method: :post,
|
58
|
+
class: "rounded-md bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50",
|
59
|
+
form: { data: { turbo_confirm: "Are you sure you want to retry this job?" } },
|
60
|
+
disabled: @job_stat.status == 'running' %>
|
61
|
+
<% end %>
|
62
|
+
|
63
|
+
<% if @job_stat.status != 'completed' %>
|
64
|
+
<%= button_to "Discard Job", discard_job_path(@job_stat.id),
|
65
|
+
method: :delete,
|
66
|
+
class: "rounded-md bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-red-300 hover:bg-gray-50 hover:text-red-700",
|
67
|
+
form: { data: { turbo_confirm: "Are you sure you want to discard this job?" } } %>
|
68
|
+
<% end %>
|
69
|
+
</div>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
|
74
|
+
<!-- Error Information Section -->
|
75
|
+
<% if @job_stat.status == 'failed' %>
|
76
|
+
<div class="bg-white shadow rounded-lg p-6">
|
77
|
+
<h3 class="text-lg font-medium text-red-700 mb-4">Error Information</h3>
|
78
|
+
|
79
|
+
<div class="space-y-6">
|
80
|
+
<!-- Error Data -->
|
81
|
+
<% if @job_stat.error.present? %>
|
82
|
+
<div>
|
83
|
+
<h4 class="text-sm font-medium text-gray-500 mb-2">Error Details</h4>
|
84
|
+
<div class="bg-red-50 p-4 rounded-md overflow-auto max-h-[500px]">
|
85
|
+
<pre class="font-mono text-xs text-red-700 whitespace-pre-wrap break-all"><%= @job_stat.error %></pre>
|
86
|
+
</div>
|
87
|
+
</div>
|
88
|
+
<% else %>
|
89
|
+
<div class="text-red-600 italic">
|
90
|
+
No error data available
|
91
|
+
</div>
|
92
|
+
<% end %>
|
93
|
+
</div>
|
94
|
+
</div>
|
95
|
+
<% end %>
|
96
|
+
<% else %>
|
97
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
98
|
+
Job not found.
|
99
|
+
</div>
|
100
|
+
<% end %>
|
101
|
+
</div>
|
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,30 +2,41 @@
|
|
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>
|
9
|
+
<% if @status.present? %>
|
10
|
+
<p class="text-gray-600 mb-2">Filtered by status: <span class="font-medium"><%= @status %></span></p>
|
11
|
+
<% end %>
|
9
12
|
|
10
13
|
<% if @queue %>
|
11
14
|
<!-- Queue Stats -->
|
12
|
-
<div class="grid grid-cols-2 md:grid-cols-
|
13
|
-
|
15
|
+
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
16
|
+
<%= link_to status_queue_path(id: @queue_name, status: "pending"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
14
17
|
<p class="text-sm text-gray-500">Pending Jobs</p>
|
15
|
-
<p class="text-2xl font-bold text-
|
16
|
-
|
17
|
-
|
18
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.pending %></p>
|
19
|
+
<% end %>
|
20
|
+
|
21
|
+
<%= link_to status_queue_path(id: @queue_name, status: "scheduled"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
18
22
|
<p class="text-sm text-gray-500">Scheduled Jobs</p>
|
19
|
-
<p class="text-2xl font-bold text-
|
20
|
-
|
21
|
-
|
23
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.scheduled %></p>
|
24
|
+
<% end %>
|
25
|
+
|
26
|
+
<%= link_to status_queue_path(id: @queue_name, status: "running"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
22
27
|
<p class="text-sm text-gray-500">Running Jobs</p>
|
23
|
-
<p class="text-2xl font-bold text-
|
24
|
-
|
25
|
-
|
28
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.running %></p>
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
<%= link_to status_queue_path(id: @queue_name, status: "failed"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
26
32
|
<p class="text-sm text-gray-500">Failed Jobs</p>
|
27
|
-
<p class="text-2xl font-bold text-
|
28
|
-
|
33
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.failed %></p>
|
34
|
+
<% end %>
|
35
|
+
|
36
|
+
<%= link_to status_queue_path(id: @queue_name, status: "completed"), class: "bg-white shadow-sm rounded p-4 hover:shadow-md transition-shadow duration-200" do %>
|
37
|
+
<p class="text-sm text-gray-500">Completed Jobs</p>
|
38
|
+
<p class="text-2xl font-bold text-blue-500"><%= @queue.completed %></p>
|
39
|
+
<% end %>
|
29
40
|
</div>
|
30
41
|
|
31
42
|
<!-- Queue Details -->
|
@@ -43,6 +54,156 @@
|
|
43
54
|
</div>
|
44
55
|
</div>
|
45
56
|
</div>
|
57
|
+
|
58
|
+
<!-- Jobs Table -->
|
59
|
+
<div class="bg-white shadow rounded p-6">
|
60
|
+
<div class="flex justify-between items-center mb-4">
|
61
|
+
<h3 class="text-xl font-semibold">
|
62
|
+
<% if @status.present? %>
|
63
|
+
Recent <%= @status.capitalize %> Jobs
|
64
|
+
<% else %>
|
65
|
+
Recent Jobs
|
66
|
+
<% end %>
|
67
|
+
</h3>
|
68
|
+
<% if @status.present? %>
|
69
|
+
<%= link_to "Clear Filter", queue_path(@queue_name), class: "text-blue-500 text-sm hover:underline" %>
|
70
|
+
<% end %>
|
71
|
+
</div>
|
72
|
+
|
73
|
+
<div class="overflow-x-auto">
|
74
|
+
<table class="min-w-full divide-y divide-gray-200">
|
75
|
+
<thead class="bg-gray-50">
|
76
|
+
<tr>
|
77
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
78
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Job Class</th>
|
79
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
80
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Arguments</th>
|
81
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
|
82
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
83
|
+
</tr>
|
84
|
+
</thead>
|
85
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
86
|
+
<% if @jobs.any? %>
|
87
|
+
<% @jobs.each do |job| %>
|
88
|
+
<tr id="job-<%= job.id %>" class="<%= 'hover:bg-gray-50' %>">
|
89
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
90
|
+
<%= link_to job.id, job_path(job.id), class: "text-blue-600 hover:text-blue-800 hover:underline" %>
|
91
|
+
</td>
|
92
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium"><%= job.class_name %></td>
|
93
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
94
|
+
<% status_style = case job.status
|
95
|
+
when 'completed' then 'bg-green-50 text-green-700 ring-1 ring-green-600/20 ring-inset'
|
96
|
+
when 'running' then 'bg-blue-50 text-blue-700 ring-1 ring-blue-700/10 ring-inset'
|
97
|
+
when 'scheduled' then 'bg-yellow-50 text-yellow-800 ring-1 ring-yellow-600/20 ring-inset'
|
98
|
+
when 'pending' then 'bg-gray-50 text-gray-600 ring-1 ring-gray-500/10 ring-inset'
|
99
|
+
when 'failed' then 'bg-red-50 text-red-700 ring-1 ring-red-600/10 ring-inset'
|
100
|
+
else 'bg-purple-50 text-purple-700 ring-1 ring-purple-700/10 ring-inset'
|
101
|
+
end %>
|
102
|
+
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium <%= status_style %>">
|
103
|
+
<%= job.status %>
|
104
|
+
</span>
|
105
|
+
</td>
|
106
|
+
<td class="px-4 py-3 text-sm text-gray-500 max-w-xs truncate"><%= job.arguments %></td>
|
107
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
108
|
+
<%= job.created_at.strftime('%Y-%m-%d %H:%M:%S') %>
|
109
|
+
</td>
|
110
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
111
|
+
<div class="flex space-x-2">
|
112
|
+
<% if job.status == 'failed' && (job.error.present? || job.backtrace.present?) %>
|
113
|
+
<%= link_to "View Details", job_path(job.id),
|
114
|
+
class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-blue-300 ring-inset hover:bg-gray-50 hover:text-blue-700" %>
|
115
|
+
<% else %>
|
116
|
+
<% if ['failed', 'scheduled', 'pending', 'completed'].include?(job.status) %>
|
117
|
+
<%= button_to "Retry", retry_job_queue_path(id: @queue_name, job_id: job.id),
|
118
|
+
method: :post,
|
119
|
+
class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50",
|
120
|
+
form: { data: { turbo_confirm: "Are you sure you want to retry this job?" } },
|
121
|
+
disabled: job.status == 'running' %>
|
122
|
+
<% end %>
|
123
|
+
|
124
|
+
<% if job.status != 'completed' %>
|
125
|
+
<%= button_to "Discard", discard_job_queue_path(id: @queue_name, job_id: job.id),
|
126
|
+
method: :delete,
|
127
|
+
class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-red-300 ring-inset hover:bg-gray-50 hover:text-red-700",
|
128
|
+
form: { data: { turbo_confirm: "Are you sure you want to discard this job?" } } %>
|
129
|
+
<% end %>
|
130
|
+
<% end %>
|
131
|
+
</div>
|
132
|
+
</td>
|
133
|
+
</tr>
|
134
|
+
<% end %>
|
135
|
+
<% else %>
|
136
|
+
<tr>
|
137
|
+
<td colspan="6" class="px-4 py-3 text-center text-sm text-gray-500">
|
138
|
+
<% if @status.present? %>
|
139
|
+
No <%= @status %> jobs found in this queue
|
140
|
+
<% else %>
|
141
|
+
No jobs found in this queue
|
142
|
+
<% end %>
|
143
|
+
</td>
|
144
|
+
</tr>
|
145
|
+
<% end %>
|
146
|
+
</tbody>
|
147
|
+
</table>
|
148
|
+
</div>
|
149
|
+
|
150
|
+
<!-- Pagination -->
|
151
|
+
<% if @pagy&.pages && @pagy.pages > 1 %>
|
152
|
+
<div class="px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
153
|
+
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
154
|
+
<div>
|
155
|
+
<p class="text-sm text-gray-700">
|
156
|
+
<% if @total_count > 0 %>
|
157
|
+
Showing <span class="font-medium"><%= @pagy.from %></span> to <span class="font-medium"><%= @pagy.to %></span> of <span class="font-medium"><%= @pagy.count %></span> results
|
158
|
+
<% else %>
|
159
|
+
No matching jobs found
|
160
|
+
<% end %>
|
161
|
+
</p>
|
162
|
+
</div>
|
163
|
+
<div>
|
164
|
+
<% if @total_count > 0 %>
|
165
|
+
<nav class="relative z-0 inline-flex shadow-sm -space-x-px" aria-label="Pagination">
|
166
|
+
<% if @pagy.prev %>
|
167
|
+
<%= link_to @status.present? ? status_queue_path(id: @queue_name, status: @status, page: @pagy.prev) : queue_path(@queue_name, page: @pagy.prev),
|
168
|
+
class: "relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50" do %>
|
169
|
+
<span class="sr-only">Previous</span>
|
170
|
+
«
|
171
|
+
<% end %>
|
172
|
+
<% else %>
|
173
|
+
<span class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-gray-100 text-sm font-medium text-gray-400 cursor-not-allowed">
|
174
|
+
<span class="sr-only">Previous</span>
|
175
|
+
«
|
176
|
+
</span>
|
177
|
+
<% end %>
|
178
|
+
|
179
|
+
<% @pagy.series.each do |item| %>
|
180
|
+
<% if item.is_a?(Integer) %>
|
181
|
+
<%= link_to item, @status.present? ? status_queue_path(id: @queue_name, status: @status, page: item) : queue_path(@queue_name, page: item),
|
182
|
+
class: "relative inline-flex items-center px-4 py-2 border #{item == @pagy.page ? 'border-blue-500 bg-blue-50 text-blue-600' : 'border-gray-300 bg-white text-gray-500 hover:bg-gray-50'} text-sm font-medium" %>
|
183
|
+
<% elsif item == :gap %>
|
184
|
+
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">...</span>
|
185
|
+
<% end %>
|
186
|
+
<% end %>
|
187
|
+
|
188
|
+
<% if @pagy.next %>
|
189
|
+
<%= link_to @status.present? ? status_queue_path(id: @queue_name, status: @status, page: @pagy.next) : queue_path(@queue_name, page: @pagy.next),
|
190
|
+
class: "relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50" do %>
|
191
|
+
<span class="sr-only">Next</span>
|
192
|
+
»
|
193
|
+
<% end %>
|
194
|
+
<% else %>
|
195
|
+
<span class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-gray-100 text-sm font-medium text-gray-400 cursor-not-allowed">
|
196
|
+
<span class="sr-only">Next</span>
|
197
|
+
»
|
198
|
+
</span>
|
199
|
+
<% end %>
|
200
|
+
</nav>
|
201
|
+
<% end %>
|
202
|
+
</div>
|
203
|
+
</div>
|
204
|
+
</div>
|
205
|
+
<% end %>
|
206
|
+
</div>
|
46
207
|
<% else %>
|
47
208
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
48
209
|
Queue not found.
|