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.
@@ -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
- SolidQueue::Job.where("run_at <= ?", Time.current).count
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
- SolidQueue::Job.where("run_at > ?", Time.current).count
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
- SolidQueue::Execution.where(finished_at: nil).count
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
- SolidQueue::FailedExecution.count
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
- SolidQueue::Execution.where.not(finished_at: nil).count
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
- oldest = SolidQueue::Job.order(:created_at).limit(1).pick(:created_at)
43
- return 0 unless oldest
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
- (Time.current - oldest).to_i
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
- queue_names.map { |q| stats_for(q) }.sort_by(&:name)
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
- SolidQueue::Job.distinct.pluck(:queue_name)
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 scheduled for now or in the past but not finished
20
- pending = SolidQueue::Job.where(queue_name: name)
21
- .where("scheduled_at <= ? AND finished_at IS NULL", Time.current)
22
- .count
23
-
24
- # Scheduled: Jobs scheduled for the future
25
- scheduled = SolidQueue::Job.where(queue_name: name)
26
- .where("scheduled_at > ? AND finished_at IS NULL", Time.current)
27
- .count
28
-
29
- # Running: Jobs that are currently claimed but not finished
30
- running = SolidQueue::ClaimedExecution.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_claimed_executions.job_id")
31
- .where("solid_queue_jobs.queue_name = ?", name)
32
- .count
33
-
34
- # Failed: Jobs that have failed executions
35
- failed = SolidQueue::FailedExecution.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_failed_executions.job_id")
36
- .where("solid_queue_jobs.queue_name = ?", name)
37
- .count
38
-
39
- # Latency: Time since oldest unfinished job was scheduled
40
- oldest_scheduled_at = SolidQueue::Job.where(queue_name: name)
41
- .where(finished_at: nil)
42
- .order(:scheduled_at)
43
- .limit(1)
44
- .pick(:scheduled_at)
45
-
46
- latency = oldest_scheduled_at ? (Time.current - oldest_scheduled_at).to_i : 0
47
-
48
- QueueStat.new(name:, pending:, scheduled:, running:, failed:, latency:)
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-sky-500"><%= metric.value %></p>
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-sky-500">
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-sky-500">
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-sky-500" %>
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-4 gap-4">
13
- <div class="bg-white shadow-sm rounded p-4">
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-sky-500"><%= @queue.pending %></p>
16
- </div>
17
- <div class="bg-white shadow-sm rounded p-4">
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-sky-500"><%= @queue.scheduled %></p>
20
- </div>
21
- <div class="bg-white shadow-sm rounded p-4">
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-sky-500"><%= @queue.running %></p>
24
- </div>
25
- <div class="bg-white shadow-sm rounded p-4">
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-sky-500"><%= @queue.failed %></p>
28
- </div>
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
+ &laquo;
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
+ &laquo;
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
+ &raquo;
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
+ &raquo;
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.