solid_queue_web 0.5.0 → 0.6.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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -8
  3. data/app/assets/stylesheets/solid_queue_web/application.css +69 -0
  4. data/app/controllers/solid_queue_web/application_controller.rb +2 -0
  5. data/app/controllers/solid_queue_web/failed_jobs/selections_controller.rb +27 -0
  6. data/app/controllers/solid_queue_web/failed_jobs_controller.rb +26 -9
  7. data/app/controllers/solid_queue_web/jobs/selections_controller.rb +21 -0
  8. data/app/controllers/solid_queue_web/jobs_controller.rb +17 -27
  9. data/app/controllers/solid_queue_web/queues/jobs_controller.rb +63 -0
  10. data/app/controllers/solid_queue_web/search_controller.rb +23 -0
  11. data/app/javascript/solid_queue_web/application.js +4 -0
  12. data/app/javascript/solid_queue_web/refresh_controller.js +51 -0
  13. data/app/javascript/solid_queue_web/search_controller.js +5 -0
  14. data/app/javascript/solid_queue_web/selection_controller.js +42 -0
  15. data/app/models/solid_queue_web/job.rb +13 -0
  16. data/app/views/layouts/solid_queue_web/application.html.erb +1 -0
  17. data/app/views/solid_queue_web/dashboard/index.html.erb +3 -1
  18. data/app/views/solid_queue_web/failed_jobs/index.html.erb +117 -43
  19. data/app/views/solid_queue_web/jobs/index.html.erb +116 -59
  20. data/app/views/solid_queue_web/jobs/show.html.erb +13 -8
  21. data/app/views/solid_queue_web/processes/index.html.erb +3 -1
  22. data/app/views/solid_queue_web/queues/jobs/destroy.turbo_stream.erb +9 -0
  23. data/app/views/solid_queue_web/queues/jobs/index.html.erb +89 -0
  24. data/app/views/solid_queue_web/search/index.html.erb +64 -0
  25. data/config/importmap.rb +2 -0
  26. data/config/routes.rb +14 -0
  27. data/lib/solid_queue_web/version.rb +1 -1
  28. metadata +11 -1
@@ -2,58 +2,132 @@
2
2
  <h1 class="sqd-page-title">Failed Jobs</h1>
3
3
  <% if @failed_jobs.any? %>
4
4
  <div class="sqd-actions">
5
- <%= button_to "Retry All", retry_all_failed_jobs_path, method: :post, class: "sqd-btn sqd-btn--primary",
5
+ <%= button_to "Retry All", retry_all_failed_jobs_path,
6
+ method: :post,
7
+ params: { queue: @queue, q: @search, period: @period },
8
+ class: "sqd-btn sqd-btn--primary",
6
9
  data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
7
- <%= button_to "Discard All", discard_all_failed_jobs_path, method: :post, class: "sqd-btn sqd-btn--danger",
10
+ <%= button_to "Discard All", discard_all_failed_jobs_path,
11
+ method: :post,
12
+ params: { queue: @queue, q: @search, period: @period },
13
+ class: "sqd-btn sqd-btn--danger",
8
14
  data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
9
15
  </div>
10
16
  <% end %>
11
17
  </div>
12
18
 
19
+ <form class="sqd-search" action="<%= failed_jobs_path %>" method="get" data-controller="search">
20
+ <% if @queue.present? %>
21
+ <input type="hidden" name="queue" value="<%= @queue %>">
22
+ <% end %>
23
+ <input type="hidden" name="period" value="<%= @period %>">
24
+ <input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
25
+ placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
26
+ data-action="input->search#filter">
27
+ <button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
28
+ <% if @search.present? %>
29
+ <%= link_to "Clear", failed_jobs_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
30
+ <% end %>
31
+ <div class="sqd-period-filter" role="group" aria-label="Time period">
32
+ <%= link_to "All", failed_jobs_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time" %>
33
+ <%= link_to "1h", failed_jobs_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour" %>
34
+ <%= link_to "24h", failed_jobs_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours" %>
35
+ <%= link_to "7d", failed_jobs_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days" %>
36
+ </div>
37
+ </form>
38
+
13
39
  <% if @pagy.last > 1 %>
14
40
  <%= @pagy.series_nav.html_safe %>
15
41
  <% end %>
16
42
 
17
- <div class="sqd-card">
18
- <% if @failed_jobs.empty? %>
19
- <div class="sqd-empty">No failed jobs. All clear!</div>
20
- <% else %>
21
- <table>
22
- <thead>
23
- <tr>
24
- <th scope="col">Job Class</th>
25
- <th scope="col">Queue</th>
26
- <th scope="col">Error</th>
27
- <th scope="col">Failed At</th>
28
- <th scope="col"><span class="sqd-sr-only">Actions</span></th>
29
- </tr>
30
- </thead>
31
- <tbody>
32
- <% @failed_jobs.each do |execution| %>
33
- <% job = execution.job %>
43
+ <% if @failed_jobs.any? %>
44
+ <div data-controller="selection">
45
+ <%= form_tag failed_job_selection_path, method: :post, id: "retry-selection-form" do %>
46
+ <%= hidden_field_tag :queue, @queue %>
47
+ <%= hidden_field_tag :q, @search %>
48
+ <%= hidden_field_tag :period, @period %>
49
+ <% end %>
50
+
51
+ <%= form_tag failed_job_selection_path, method: :delete, id: "discard-selection-form",
52
+ data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do %>
53
+ <%= hidden_field_tag :queue, @queue %>
54
+ <%= hidden_field_tag :q, @search %>
55
+ <%= hidden_field_tag :period, @period %>
56
+ <% end %>
57
+
58
+ <div class="sqd-selection-bar" data-selection-target="bar" style="display: none;">
59
+ <span class="sqd-muted-text"><span data-selection-target="count">0</span> selected</span>
60
+ <button type="button" class="sqd-btn sqd-btn--primary sqd-btn--sm"
61
+ data-action="click->selection#submit"
62
+ data-selection-form-id-param="retry-selection-form">Retry Selected</button>
63
+ <button type="button" class="sqd-btn sqd-btn--danger sqd-btn--sm"
64
+ data-action="click->selection#submit"
65
+ data-selection-form-id-param="discard-selection-form">Discard Selected</button>
66
+ </div>
67
+
68
+ <div class="sqd-card">
69
+ <table>
70
+ <thead>
34
71
  <tr>
35
- <td><%= link_to job.class_name, job_path(job) %></td>
36
- <td class="sqd-mono"><%= job.queue_name %></td>
37
- <td>
38
- <% if execution.exception_class.present? %>
39
- <div class="sqd-error-msg sqd-truncate" title="<%= execution.exception_class %>: <%= execution.message %>">
40
- <strong><%= execution.exception_class %></strong>: <%= execution.message %>
41
- </div>
42
- <% else %>
43
- <span style="color:var(--muted)">—</span>
44
- <% end %>
45
- </td>
46
- <td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
47
- <td class="sqd-row-actions">
48
- <%= button_to "Retry", retry_failed_job_path(execution), method: :post,
49
- class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
50
- <%= button_to "Discard", failed_job_path(execution), method: :delete,
51
- class: "sqd-btn sqd-btn--danger sqd-btn--sm",
52
- data: { confirm: "Discard this job?" } %>
53
- </td>
72
+ <th scope="col">
73
+ <input type="checkbox" data-selection-target="selectAll"
74
+ data-action="change->selection#selectAll"
75
+ aria-label="Select all failed jobs">
76
+ </th>
77
+ <th scope="col">Job Class</th>
78
+ <th scope="col">Queue</th>
79
+ <th scope="col">Error</th>
80
+ <th scope="col">Failed At</th>
81
+ <th scope="col"><span class="sqd-sr-only">Actions</span></th>
54
82
  </tr>
55
- <% end %>
56
- </tbody>
57
- </table>
58
- <% end %>
59
- </div>
83
+ </thead>
84
+ <tbody>
85
+ <% @failed_jobs.each do |execution| %>
86
+ <% job = execution.job %>
87
+ <tr>
88
+ <td>
89
+ <input type="checkbox" value="<%= execution.id %>"
90
+ data-selection-target="checkbox"
91
+ data-action="change->selection#toggle"
92
+ aria-label="Select job <%= job.class_name %>">
93
+ </td>
94
+ <td><%= link_to job.class_name, job_path(job) %></td>
95
+ <td>
96
+ <%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search, period: @period),
97
+ class: "sqd-mono", style: "color: inherit;" %>
98
+ </td>
99
+ <td>
100
+ <% if execution.exception_class.present? %>
101
+ <div class="sqd-error-msg sqd-truncate" title="<%= execution.exception_class %>: <%= execution.message %>">
102
+ <strong><%= execution.exception_class %></strong>: <%= execution.message %>
103
+ </div>
104
+ <% else %>
105
+ <span style="color:var(--muted)">—</span>
106
+ <% end %>
107
+ </td>
108
+ <td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
109
+ <td class="sqd-row-actions">
110
+ <%= button_to "Retry", retry_failed_job_path(execution), method: :post,
111
+ class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
112
+ <%= button_to "Discard", failed_job_path(execution), method: :delete,
113
+ class: "sqd-btn sqd-btn--danger sqd-btn--sm",
114
+ data: { confirm: "Discard this job?" } %>
115
+ </td>
116
+ </tr>
117
+ <% end %>
118
+ </tbody>
119
+ </table>
120
+ </div>
121
+ </div>
122
+ <% else %>
123
+ <div class="sqd-card">
124
+ <div class="sqd-empty">No failed jobs. All clear!</div>
125
+ </div>
126
+ <% end %>
127
+
128
+ <% if @queue.present? %>
129
+ <p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
130
+ Filtering by queue: <strong><%= @queue %></strong> &mdash;
131
+ <%= link_to "Clear filter", failed_jobs_path(q: @search, period: @period) %>
132
+ </p>
133
+ <% end %>
@@ -1,21 +1,21 @@
1
1
  <h1 class="sqd-page-title" style="margin-bottom: 1.5rem;">Jobs</h1>
2
2
 
3
- <%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance" } do %>
4
- <% discardable = SolidQueueWeb::JobsController::DISCARDABLE.include?(@status) %>
3
+ <%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance", controller: "refresh", refresh_interval_value: 10000 } do %>
4
+ <% discardable = SolidQueueWeb::Job::DISCARDABLE.include?(@status) %>
5
5
 
6
6
  <div class="sqd-page-header">
7
7
  <div class="sqd-filters">
8
- <%= link_to "Ready", jobs_path(status: "ready", queue: @queue, q: @search), class: @status == "ready" ? "active" : "" %>
9
- <%= link_to "Scheduled", jobs_path(status: "scheduled", queue: @queue, q: @search), class: @status == "scheduled" ? "active" : "" %>
10
- <%= link_to "Running", jobs_path(status: "claimed", queue: @queue, q: @search), class: @status == "claimed" ? "active" : "" %>
11
- <%= link_to "Blocked", jobs_path(status: "blocked", queue: @queue, q: @search), class: @status == "blocked" ? "active" : "" %>
12
- <%= link_to "Failed", jobs_path(status: "failed", queue: @queue, q: @search), class: @status == "failed" ? "active" : "" %>
8
+ <%= link_to "Ready", jobs_path(status: "ready", q: @search, period: @period), class: @status == "ready" ? "active" : "" %>
9
+ <%= link_to "Scheduled", jobs_path(status: "scheduled", q: @search, period: @period), class: @status == "scheduled" ? "active" : "" %>
10
+ <%= link_to "Running", jobs_path(status: "claimed", q: @search, period: @period), class: @status == "claimed" ? "active" : "" %>
11
+ <%= link_to "Blocked", jobs_path(status: "blocked", q: @search, period: @period), class: @status == "blocked" ? "active" : "" %>
12
+ <%= link_to "Failed", jobs_path(status: "failed", q: @search, period: @period), class: @status == "failed" ? "active" : "" %>
13
13
  </div>
14
14
  <% if discardable && @jobs.any? %>
15
15
  <div class="sqd-actions">
16
16
  <%= button_to "Discard All", discard_all_jobs_path,
17
17
  method: :post,
18
- params: { status: @status, queue: @queue },
18
+ params: { status: @status, period: @period },
19
19
  class: "sqd-btn sqd-btn--danger",
20
20
  data: { confirm: "Discard all #{@jobs.size} #{@status} jobs? This cannot be undone." } %>
21
21
  </div>
@@ -24,74 +24,131 @@
24
24
 
25
25
  <form class="sqd-search" action="<%= jobs_path %>" method="get" data-controller="search">
26
26
  <input type="hidden" name="status" value="<%= @status %>">
27
- <% if @queue.present? %>
28
- <input type="hidden" name="queue" value="<%= @queue %>">
29
- <% end %>
27
+ <input type="hidden" name="period" value="<%= @period %>">
30
28
  <input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
31
29
  placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
32
30
  data-action="input->search#filter">
33
31
  <button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
34
32
  <% if @search.present? %>
35
- <%= link_to "Clear", jobs_path(status: @status, queue: @queue), class: "sqd-btn sqd-btn--muted" %>
33
+ <%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqd-btn sqd-btn--muted" %>
36
34
  <% end %>
35
+ <div class="sqd-period-filter" role="group" aria-label="Time period">
36
+ <%= link_to "All", jobs_path(status: @status, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time" %>
37
+ <%= link_to "1h", jobs_path(status: @status, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour" %>
38
+ <%= link_to "24h", jobs_path(status: @status, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours" %>
39
+ <%= link_to "7d", jobs_path(status: @status, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days" %>
40
+ </div>
37
41
  </form>
38
42
 
39
- <div class="sqd-card" id="jobs-list">
40
- <% if @jobs.empty? %>
41
- <div class="sqd-empty">No <%= @status %> jobs.</div>
42
- <% else %>
43
- <table>
44
- <thead>
45
- <tr>
46
- <th scope="col">Job Class</th>
47
- <th scope="col">Queue</th>
48
- <th scope="col">Priority</th>
49
- <th scope="col">Scheduled At</th>
50
- <th scope="col">Enqueued At</th>
51
- <% if discardable %><th scope="col"><span class="sqd-sr-only">Actions</span></th><% end %>
52
- </tr>
53
- </thead>
54
- <tbody>
55
- <% @jobs.each do |execution| %>
56
- <% job = execution.job %>
57
- <tr id="execution_<%= execution.id %>">
58
- <td>
59
- <span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
60
- <%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;" %>
61
- </td>
62
- <td>
63
- <%= link_to job.queue_name, jobs_path(status: @status, queue: job.queue_name),
64
- class: "sqd-mono", style: "color: inherit;" %>
65
- </td>
66
- <td><%= job.priority %></td>
67
- <td class="sqd-mono">
68
- <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "" %>
69
- </td>
70
- <td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
71
- <% if discardable %>
43
+ <% if discardable && @jobs.any? %>
44
+ <div data-controller="selection">
45
+ <%= form_tag job_selection_path, method: :delete, id: "job-selection-form",
46
+ data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do %>
47
+ <%= hidden_field_tag :status, @status %>
48
+ <%= hidden_field_tag :period, @period %>
49
+ <% end %>
50
+
51
+ <div class="sqd-selection-bar" data-selection-target="bar" style="display: none;">
52
+ <span class="sqd-muted-text"><span data-selection-target="count">0</span> selected</span>
53
+ <button type="button" class="sqd-btn sqd-btn--danger sqd-btn--sm"
54
+ data-action="click->selection#submit"
55
+ data-selection-form-id-param="job-selection-form">Discard Selected</button>
56
+ </div>
57
+
58
+ <div class="sqd-card" id="jobs-list">
59
+ <table>
60
+ <thead>
61
+ <tr>
62
+ <th scope="col">
63
+ <input type="checkbox" data-selection-target="selectAll"
64
+ data-action="change->selection#selectAll"
65
+ aria-label="Select all jobs">
66
+ </th>
67
+ <th scope="col">Job Class</th>
68
+ <th scope="col">Queue</th>
69
+ <th scope="col">Priority</th>
70
+ <th scope="col">Scheduled At</th>
71
+ <th scope="col">Enqueued At</th>
72
+ <th scope="col"><span class="sqd-sr-only">Actions</span></th>
73
+ </tr>
74
+ </thead>
75
+ <tbody>
76
+ <% @jobs.each do |execution| %>
77
+ <% job = execution.job %>
78
+ <tr id="execution_<%= execution.id %>">
79
+ <td>
80
+ <input type="checkbox" value="<%= execution.id %>"
81
+ data-selection-target="checkbox"
82
+ data-action="change->selection#toggle"
83
+ aria-label="Select job <%= job.class_name %>">
84
+ </td>
85
+ <td>
86
+ <span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
87
+ <%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
88
+ </td>
89
+ <td>
90
+ <%= link_to job.queue_name, queue_jobs_path(queue_name: job.queue_name, status: @status),
91
+ class: "sqd-mono", style: "color: inherit;" %>
92
+ </td>
93
+ <td><%= job.priority %></td>
94
+ <td class="sqd-mono">
95
+ <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
96
+ </td>
97
+ <td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
72
98
  <td class="sqd-row-actions">
73
99
  <%= button_to "Discard", job_path(execution),
74
100
  method: :delete,
75
- params: { status: @status, queue: @queue },
101
+ params: { status: @status, period: @period },
76
102
  class: "sqd-btn sqd-btn--danger sqd-btn--sm",
77
103
  data: { confirm: "Discard this job?" } %>
78
104
  </td>
79
- <% end %>
105
+ </tr>
106
+ <% end %>
107
+ </tbody>
108
+ </table>
109
+ </div>
110
+ </div>
111
+ <% else %>
112
+ <div class="sqd-card" id="jobs-list">
113
+ <% if @jobs.empty? %>
114
+ <div class="sqd-empty">No <%= @status %> jobs.</div>
115
+ <% else %>
116
+ <table>
117
+ <thead>
118
+ <tr>
119
+ <th scope="col">Job Class</th>
120
+ <th scope="col">Queue</th>
121
+ <th scope="col">Priority</th>
122
+ <th scope="col">Scheduled At</th>
123
+ <th scope="col">Enqueued At</th>
80
124
  </tr>
81
- <% end %>
82
- </tbody>
83
- </table>
84
- <% end %>
85
- </div>
125
+ </thead>
126
+ <tbody>
127
+ <% @jobs.each do |execution| %>
128
+ <% job = execution.job %>
129
+ <tr id="execution_<%= execution.id %>">
130
+ <td>
131
+ <span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
132
+ <%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
133
+ </td>
134
+ <td>
135
+ <%= link_to job.queue_name, queue_jobs_path(queue_name: job.queue_name, status: @status),
136
+ class: "sqd-mono", style: "color: inherit;" %>
137
+ </td>
138
+ <td><%= job.priority %></td>
139
+ <td class="sqd-mono">
140
+ <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
141
+ </td>
142
+ <td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
143
+ </tr>
144
+ <% end %>
145
+ </tbody>
146
+ </table>
147
+ <% end %>
148
+ </div>
149
+ <% end %>
86
150
 
87
151
  <% if @pagy.last > 1 %>
88
152
  <%= @pagy.series_nav.html_safe %>
89
153
  <% end %>
90
-
91
- <% if @queue.present? %>
92
- <p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
93
- Filtering by queue: <strong><%= @queue %></strong> &mdash;
94
- <%= link_to "Clear filter", jobs_path(status: @status) %>
95
- </p>
96
- <% end %>
97
154
  <% end %>
@@ -7,13 +7,13 @@
7
7
  </div>
8
8
 
9
9
  <div class="sqd-actions">
10
- <% if @execution_status == "failed" && @failed_execution %>
11
- <%= button_to "Retry", retry_failed_job_path(@failed_execution), method: :post,
10
+ <% if @execution_status == "failed" && @job.failed_execution %>
11
+ <%= button_to "Retry", retry_failed_job_path(@job.failed_execution), method: :post,
12
12
  class: "sqd-btn sqd-btn--primary" %>
13
- <%= button_to "Discard", failed_job_path(@failed_execution), method: :delete,
13
+ <%= button_to "Discard", failed_job_path(@job.failed_execution), method: :delete,
14
14
  class: "sqd-btn sqd-btn--danger",
15
15
  data: { confirm: "Discard this job?" } %>
16
- <% elsif SolidQueueWeb::JobsController::DISCARDABLE.include?(@execution_status) %>
16
+ <% elsif SolidQueueWeb::Job::DISCARDABLE.include?(@execution_status) %>
17
17
  <% execution = @job.public_send("#{@execution_status}_execution") %>
18
18
  <% if execution %>
19
19
  <%= button_to "Discard", job_path(execution),
@@ -45,6 +45,11 @@
45
45
  <dt>Concurrency Key</dt>
46
46
  <dd class="sqd-mono"><%= @job.concurrency_key.presence || "—" %></dd>
47
47
 
48
+ <% if @job.blocked_execution %>
49
+ <dt>Blocked Until</dt>
50
+ <dd class="sqd-mono"><%= @job.blocked_execution.expires_at ? @job.blocked_execution.expires_at.strftime("%Y-%m-%d %H:%M:%S %Z") : "—" %></dd>
51
+ <% end %>
52
+
48
53
  <dt>Enqueued At</dt>
49
54
  <dd class="sqd-mono"><%= @job.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></dd>
50
55
 
@@ -62,14 +67,14 @@
62
67
  </div>
63
68
  </div>
64
69
 
65
- <% if @failed_execution %>
70
+ <% if @job.failed_execution %>
66
71
  <div class="sqd-card sqd-detail-section" style="margin-top: 1.5rem;">
67
72
  <h2 class="sqd-section-title sqd-section-title--danger">Error</h2>
68
73
  <p class="sqd-error-header">
69
- <strong><%= @failed_execution.exception_class %></strong>: <%= @failed_execution.message %>
74
+ <strong><%= @job.failed_execution.exception_class %></strong>: <%= @job.failed_execution.message %>
70
75
  </p>
71
- <% if @failed_execution.backtrace.present? %>
72
- <pre class="sqd-pre sqd-pre--muted" style="margin-top: 0.75rem;"><%= Array(@failed_execution.backtrace).join("\n") %></pre>
76
+ <% if @job.failed_execution.backtrace.present? %>
77
+ <pre class="sqd-pre sqd-pre--muted" style="margin-top: 0.75rem;"><%= Array(@job.failed_execution.backtrace).join("\n") %></pre>
73
78
  <% end %>
74
79
  </div>
75
80
  <% end %>
@@ -1,3 +1,4 @@
1
+ <%= turbo_frame_tag "processes", target: "_top", data: { controller: "refresh", refresh_interval_value: 10000 } do %>
1
2
  <h1 class="sqd-page-title">Processes</h1>
2
3
 
3
4
  <div class="sqd-card">
@@ -51,4 +52,5 @@
51
52
  </tbody>
52
53
  </table>
53
54
  <% end %>
54
- </div>
55
+ </div>
56
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <% if @remaining_count == 0 %>
2
+ <%= turbo_stream.replace "jobs-list" do %>
3
+ <div class="sqd-card" id="jobs-list">
4
+ <div class="sqd-empty">No <%= @status %> jobs in <%= @queue %>.</div>
5
+ </div>
6
+ <% end %>
7
+ <% else %>
8
+ <%= turbo_stream.remove "execution_#{@execution.id}" %>
9
+ <% end %>
@@ -0,0 +1,89 @@
1
+ <div class="sqd-page-header">
2
+ <div>
3
+ <div class="sqd-breadcrumb">
4
+ <%= link_to "Queues", queues_path %> &rsaquo; <%= @queue %>
5
+ </div>
6
+ <h1 class="sqd-page-title">Jobs</h1>
7
+ </div>
8
+ </div>
9
+
10
+ <%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance" } do %>
11
+ <% discardable = SolidQueueWeb::Job::DISCARDABLE.include?(@status) %>
12
+
13
+ <div class="sqd-page-header">
14
+ <div class="sqd-filters">
15
+ <%= link_to "Ready", queue_jobs_path(queue_name: @queue, status: "ready", q: @search), class: @status == "ready" ? "active" : "" %>
16
+ <%= link_to "Scheduled", queue_jobs_path(queue_name: @queue, status: "scheduled", q: @search), class: @status == "scheduled" ? "active" : "" %>
17
+ <%= link_to "Running", queue_jobs_path(queue_name: @queue, status: "claimed", q: @search), class: @status == "claimed" ? "active" : "" %>
18
+ <%= link_to "Blocked", queue_jobs_path(queue_name: @queue, status: "blocked", q: @search), class: @status == "blocked" ? "active" : "" %>
19
+ <%= link_to "Failed", queue_jobs_path(queue_name: @queue, status: "failed", q: @search), class: @status == "failed" ? "active" : "" %>
20
+ </div>
21
+ <% if discardable && @jobs.any? %>
22
+ <div class="sqd-actions">
23
+ <%= button_to "Discard All", discard_all_queue_jobs_path(queue_name: @queue),
24
+ method: :post,
25
+ params: { status: @status },
26
+ class: "sqd-btn sqd-btn--danger",
27
+ data: { confirm: "Discard all #{@jobs.size} #{@status} jobs in #{@queue}? This cannot be undone." } %>
28
+ </div>
29
+ <% end %>
30
+ </div>
31
+
32
+ <form class="sqd-search" action="<%= queue_jobs_path(queue_name: @queue) %>" method="get" data-controller="search">
33
+ <input type="hidden" name="status" value="<%= @status %>">
34
+ <input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
35
+ placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
36
+ data-action="input->search#filter">
37
+ <button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
38
+ <% if @search.present? %>
39
+ <%= link_to "Clear", queue_jobs_path(queue_name: @queue, status: @status), class: "sqd-btn sqd-btn--muted" %>
40
+ <% end %>
41
+ </form>
42
+
43
+ <div class="sqd-card" id="jobs-list">
44
+ <% if @jobs.empty? %>
45
+ <div class="sqd-empty">No <%= @status %> jobs in <%= @queue %>.</div>
46
+ <% else %>
47
+ <table>
48
+ <thead>
49
+ <tr>
50
+ <th scope="col">Job Class</th>
51
+ <th scope="col">Priority</th>
52
+ <th scope="col">Scheduled At</th>
53
+ <th scope="col">Enqueued At</th>
54
+ <% if discardable %><th scope="col"><span class="sqd-sr-only">Actions</span></th><% end %>
55
+ </tr>
56
+ </thead>
57
+ <tbody>
58
+ <% @jobs.each do |execution| %>
59
+ <% job = execution.job %>
60
+ <tr id="execution_<%= execution.id %>">
61
+ <td>
62
+ <span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
63
+ <%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
64
+ </td>
65
+ <td><%= job.priority %></td>
66
+ <td class="sqd-mono">
67
+ <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
68
+ </td>
69
+ <td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
70
+ <% if discardable %>
71
+ <td class="sqd-row-actions">
72
+ <%= button_to "Discard", queue_job_path(queue_name: @queue, id: execution),
73
+ method: :delete,
74
+ params: { status: @status },
75
+ class: "sqd-btn sqd-btn--danger sqd-btn--sm",
76
+ data: { confirm: "Discard this job?" } %>
77
+ </td>
78
+ <% end %>
79
+ </tr>
80
+ <% end %>
81
+ </tbody>
82
+ </table>
83
+ <% end %>
84
+ </div>
85
+
86
+ <% if @pagy.last > 1 %>
87
+ <%= @pagy.series_nav.html_safe %>
88
+ <% end %>
89
+ <% end %>
@@ -0,0 +1,64 @@
1
+ <h1 class="sqd-page-title" style="margin-bottom: 1.5rem;">Search Jobs</h1>
2
+
3
+ <datalist id="job-class-list">
4
+ <% @job_classes.each do |klass| %>
5
+ <option value="<%= klass %>">
6
+ <% end %>
7
+ </datalist>
8
+
9
+ <form class="sqd-search sqd-search--global" action="<%= search_path %>" method="get" data-controller="search">
10
+ <input class="sqd-search__input sqd-search__input--lg" type="search" name="q"
11
+ value="<%= @query %>" placeholder="Search by job class name…"
12
+ list="job-class-list" autocomplete="off"
13
+ aria-label="Search all jobs by class name"
14
+ data-action="change->search#select">
15
+ <% if @query.present? %>
16
+ <%= link_to "Clear", search_path, class: "sqd-btn sqd-btn--muted" %>
17
+ <% end %>
18
+ </form>
19
+
20
+ <% if @query.present? %>
21
+ <% if @results.empty? %>
22
+ <div class="sqd-empty" style="margin-top: 1rem;">No jobs found matching &ldquo;<%= @query %>&rdquo;.</div>
23
+ <% else %>
24
+ <% @results.each do |status, data| %>
25
+ <div class="sqd-search-group">
26
+ <div class="sqd-search-group__header">
27
+ <span class="sqd-badge sqd-badge--<%= status %>"><%= status %></span>
28
+ <span class="sqd-muted-text">
29
+ <%= pluralize(data[:total], "match", "matches") %>
30
+ <% if data[:total] > SolidQueueWeb::SearchController::LIMIT %>
31
+ &mdash; showing first <%= SolidQueueWeb::SearchController::LIMIT %>
32
+ <% end %>
33
+ </span>
34
+ <% if status == "failed" %>
35
+ <%= link_to "View all →", failed_jobs_path(q: @query), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
36
+ <% else %>
37
+ <%= link_to "View all →", jobs_path(status: status, q: @query), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
38
+ <% end %>
39
+ </div>
40
+ <div class="sqd-card">
41
+ <table>
42
+ <thead>
43
+ <tr>
44
+ <th scope="col">Job Class</th>
45
+ <th scope="col">Queue</th>
46
+ <th scope="col">Enqueued At</th>
47
+ </tr>
48
+ </thead>
49
+ <tbody>
50
+ <% data[:executions].each do |execution| %>
51
+ <% job = execution.job %>
52
+ <tr>
53
+ <td><%= link_to job.class_name, job_path(job) %></td>
54
+ <td class="sqd-mono"><%= job.queue_name %></td>
55
+ <td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
56
+ </tr>
57
+ <% end %>
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ </div>
62
+ <% end %>
63
+ <% end %>
64
+ <% end %>
data/config/importmap.rb CHANGED
@@ -1,2 +1,4 @@
1
1
  pin "solid_queue_web", to: "solid_queue_web/application.js"
2
2
  pin "solid_queue_web/search_controller", to: "solid_queue_web/search_controller.js"
3
+ pin "solid_queue_web/refresh_controller", to: "solid_queue_web/refresh_controller.js"
4
+ pin "solid_queue_web/selection_controller", to: "solid_queue_web/selection_controller.js"