solid_queue_monitor 1.2.2 → 2.0.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -1
  3. data/app/assets/javascripts/solid_queue_monitor/application.js +393 -0
  4. data/app/{services/solid_queue_monitor/stylesheet_generator.rb → assets/stylesheets/solid_queue_monitor/application.css} +80 -12
  5. data/app/controllers/solid_queue_monitor/application_controller.rb +2 -2
  6. data/app/controllers/solid_queue_monitor/assets_controller.rb +52 -0
  7. data/app/controllers/solid_queue_monitor/base_controller.rb +0 -28
  8. data/app/controllers/solid_queue_monitor/failed_jobs_controller.rb +3 -6
  9. data/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb +3 -6
  10. data/app/controllers/solid_queue_monitor/jobs_controller.rb +3 -6
  11. data/app/controllers/solid_queue_monitor/overview_controller.rb +3 -12
  12. data/app/controllers/solid_queue_monitor/queues_controller.rb +4 -18
  13. data/app/controllers/solid_queue_monitor/ready_jobs_controller.rb +3 -6
  14. data/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb +3 -6
  15. data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +3 -6
  16. data/app/controllers/solid_queue_monitor/search_controller.rb +3 -4
  17. data/app/controllers/solid_queue_monitor/workers_controller.rb +24 -8
  18. data/app/helpers/solid_queue_monitor/application_helper.rb +46 -0
  19. data/app/helpers/solid_queue_monitor/chart_helper.rb +293 -0
  20. data/app/helpers/solid_queue_monitor/job_details_helper.rb +66 -0
  21. data/app/helpers/solid_queue_monitor/jobs_helper.rb +134 -0
  22. data/app/helpers/solid_queue_monitor/pagination_helper.rb +23 -0
  23. data/app/helpers/solid_queue_monitor/sort_helper.rb +30 -0
  24. data/app/helpers/solid_queue_monitor/workers_helper.rb +88 -0
  25. data/app/services/solid_queue_monitor/asset_cache.rb +56 -0
  26. data/app/services/solid_queue_monitor/chart_data_service.rb +2 -2
  27. data/app/views/layouts/solid_queue_monitor/application.html.erb +25 -0
  28. data/app/views/solid_queue_monitor/failed_jobs/_row.html.erb +26 -0
  29. data/app/views/solid_queue_monitor/failed_jobs/index.html.erb +38 -0
  30. data/app/views/solid_queue_monitor/in_progress_jobs/_row.html.erb +13 -0
  31. data/app/views/solid_queue_monitor/in_progress_jobs/index.html.erb +25 -0
  32. data/app/views/solid_queue_monitor/jobs/_arguments.html.erb +9 -0
  33. data/app/views/solid_queue_monitor/jobs/_error.html.erb +26 -0
  34. data/app/views/solid_queue_monitor/jobs/_execution_history.html.erb +25 -0
  35. data/app/views/solid_queue_monitor/jobs/_header.html.erb +37 -0
  36. data/app/views/solid_queue_monitor/jobs/_metadata.html.erb +22 -0
  37. data/app/views/solid_queue_monitor/jobs/_raw_data.html.erb +11 -0
  38. data/app/views/solid_queue_monitor/jobs/_timeline.html.erb +29 -0
  39. data/app/views/solid_queue_monitor/jobs/_timing.html.erb +9 -0
  40. data/app/views/solid_queue_monitor/jobs/_worker.html.erb +12 -0
  41. data/app/views/solid_queue_monitor/jobs/show.html.erb +22 -0
  42. data/app/views/solid_queue_monitor/overview/_chart.html.erb +1 -0
  43. data/app/views/solid_queue_monitor/overview/_recent_job_row.html.erb +26 -0
  44. data/app/views/solid_queue_monitor/overview/_recent_jobs.html.erb +31 -0
  45. data/app/views/solid_queue_monitor/overview/_stat_card.html.erb +4 -0
  46. data/app/views/solid_queue_monitor/overview/_stats.html.erb +11 -0
  47. data/app/views/solid_queue_monitor/overview/index.html.erb +9 -0
  48. data/app/views/solid_queue_monitor/queues/_job_row.html.erb +26 -0
  49. data/app/views/solid_queue_monitor/queues/_row.html.erb +33 -0
  50. data/app/views/solid_queue_monitor/queues/index.html.erb +18 -0
  51. data/app/views/solid_queue_monitor/queues/show.html.erb +63 -0
  52. data/app/views/solid_queue_monitor/ready_jobs/_row.html.erb +7 -0
  53. data/app/views/solid_queue_monitor/ready_jobs/index.html.erb +25 -0
  54. data/app/views/solid_queue_monitor/recurring_jobs/_row.html.erb +8 -0
  55. data/app/views/solid_queue_monitor/recurring_jobs/index.html.erb +26 -0
  56. data/app/views/solid_queue_monitor/scheduled_jobs/_row.html.erb +7 -0
  57. data/app/views/solid_queue_monitor/scheduled_jobs/index.html.erb +36 -0
  58. data/app/views/solid_queue_monitor/search/_completed_row.html.erb +6 -0
  59. data/app/views/solid_queue_monitor/search/_failed_row.html.erb +6 -0
  60. data/app/views/solid_queue_monitor/search/_job_row.html.erb +9 -0
  61. data/app/views/solid_queue_monitor/search/_recurring_row.html.erb +6 -0
  62. data/app/views/solid_queue_monitor/search/_section.html.erb +25 -0
  63. data/app/views/solid_queue_monitor/search/index.html.erb +23 -0
  64. data/app/views/solid_queue_monitor/shared/_filters.html.erb +48 -0
  65. data/app/views/solid_queue_monitor/shared/_flash.html.erb +17 -0
  66. data/app/views/solid_queue_monitor/shared/_footer.html.erb +3 -0
  67. data/app/views/solid_queue_monitor/shared/_header.html.erb +81 -0
  68. data/app/views/solid_queue_monitor/shared/_jobs_table.html.erb +20 -0
  69. data/app/views/solid_queue_monitor/shared/_pagination.html.erb +25 -0
  70. data/app/views/solid_queue_monitor/workers/_row.html.erb +22 -0
  71. data/app/views/solid_queue_monitor/workers/index.html.erb +82 -0
  72. data/config/routes.rb +6 -1
  73. data/lib/solid_queue_monitor/engine.rb +2 -0
  74. data/lib/solid_queue_monitor/version.rb +1 -1
  75. metadata +57 -17
  76. data/app/presenters/solid_queue_monitor/base_presenter.rb +0 -211
  77. data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +0 -312
  78. data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +0 -84
  79. data/app/presenters/solid_queue_monitor/job_details_presenter.rb +0 -696
  80. data/app/presenters/solid_queue_monitor/jobs_presenter.rb +0 -144
  81. data/app/presenters/solid_queue_monitor/queue_details_presenter.rb +0 -195
  82. data/app/presenters/solid_queue_monitor/queues_presenter.rb +0 -89
  83. data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +0 -81
  84. data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +0 -81
  85. data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +0 -173
  86. data/app/presenters/solid_queue_monitor/search_results_presenter.rb +0 -190
  87. data/app/presenters/solid_queue_monitor/stats_presenter.rb +0 -36
  88. data/app/presenters/solid_queue_monitor/workers_presenter.rb +0 -320
  89. data/app/services/solid_queue_monitor/chart_presenter.rb +0 -239
  90. data/app/services/solid_queue_monitor/html_generator.rb +0 -401
@@ -1,312 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidQueueMonitor
4
- class FailedJobsPresenter < BasePresenter
5
- include Rails.application.routes.url_helpers
6
- include SolidQueueMonitor::Engine.routes.url_helpers
7
-
8
- def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
9
- @jobs = jobs
10
- @current_page = current_page
11
- @total_pages = total_pages
12
- @filters = filters
13
- @sort = sort
14
- end
15
-
16
- def render
17
- section_wrapper('Failed Jobs',
18
- generate_filter_form + generate_table + generate_pagination(@current_page, @total_pages))
19
- end
20
-
21
- private
22
-
23
- def generate_filter_form
24
- <<-HTML
25
- <div class="filter-form-container">
26
- <form method="get" action="#{failed_jobs_path}" class="filter-form">
27
- <div class="filter-group">
28
- <label for="class_name">Job Class:</label>
29
- <input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
30
- </div>
31
-
32
- <div class="filter-group">
33
- <label for="queue_name">Queue:</label>
34
- <input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
35
- </div>
36
-
37
- <div class="filter-group">
38
- <label for="arguments">Arguments:</label>
39
- <input type="text" name="arguments" id="arguments" value="#{@filters[:arguments]}" placeholder="Filter by arguments">
40
- </div>
41
-
42
- <div class="filter-actions">
43
- <button type="submit" class="filter-button">Apply Filters</button>
44
- <a href="#{failed_jobs_path}" class="reset-button">Reset</a>
45
- </div>
46
- </form>
47
- </div>
48
-
49
- <div class="bulk-actions-bar">
50
- <button type="button" class="action-button retry-button" id="retry-selected-top" disabled>Retry Selected</button>
51
- <button type="button" class="action-button discard-button" id="discard-selected-top" disabled>Discard Selected</button>
52
- </div>
53
- HTML
54
- end
55
-
56
- def generate_table
57
- <<-HTML
58
- <form method="post" id="failed-jobs-form">
59
- <div class="table-container">
60
- <table>
61
- <thead>
62
- <tr>
63
- <th><input type="checkbox" id="select-all" class="select-all-checkbox"></th>
64
- #{sortable_header('class_name', 'Job')}
65
- #{sortable_header('queue_name', 'Queue')}
66
- <th>Error</th>
67
- <th>Arguments</th>
68
- #{sortable_header('created_at', 'Failed At')}
69
- <th>Actions</th>
70
- </tr>
71
- </thead>
72
- <tbody>
73
- #{@jobs.map { |failed_execution| generate_row(failed_execution) }.join}
74
- </tbody>
75
- </table>
76
- </div>
77
- </form>
78
-
79
- <script>
80
- document.addEventListener('DOMContentLoaded', function() {
81
- // Handle select all checkboxes
82
- const selectAllHeader = document.getElementById('select-all');
83
- const checkboxes = document.querySelectorAll('.job-checkbox');
84
- const retrySelectedBtn = document.getElementById('retry-selected-top');
85
- const discardSelectedBtn = document.getElementById('discard-selected-top');
86
- const form = document.getElementById('failed-jobs-form');
87
- #{' '}
88
- function updateButtonState() {
89
- const checkedBoxes = document.querySelectorAll('.job-checkbox:checked');
90
- retrySelectedBtn.disabled = checkedBoxes.length === 0;
91
- discardSelectedBtn.disabled = checkedBoxes.length === 0;
92
- }
93
- #{' '}
94
- function toggleAll(checked) {
95
- checkboxes.forEach(checkbox => {
96
- checkbox.checked = checked;
97
- });
98
- selectAllHeader.checked = checked;
99
- updateButtonState();
100
- }
101
- #{' '}
102
- selectAllHeader.addEventListener('change', function() {
103
- toggleAll(this.checked);
104
- });
105
- #{' '}
106
- checkboxes.forEach(checkbox => {
107
- checkbox.addEventListener('change', function() {
108
- updateButtonState();
109
- #{' '}
110
- // Update select all checkboxes if needed
111
- const allChecked = document.querySelectorAll('.job-checkbox:checked').length === checkboxes.length;
112
- selectAllHeader.checked = allChecked;
113
- });
114
- });
115
- #{' '}
116
- // Handle bulk actions
117
- retrySelectedBtn.addEventListener('click', function() {
118
- const selectedIds = Array.from(document.querySelectorAll('.job-checkbox:checked')).map(cb => cb.value);
119
- if (selectedIds.length === 0) return;
120
- #{' '}
121
- if (confirm('Are you sure you want to retry the selected jobs?')) {
122
- form.action = '#{retry_failed_jobs_path}';
123
- #{' '}
124
- // Add a special flag to indicate this should redirect properly
125
- const redirectInput = document.createElement('input');
126
- redirectInput.type = 'hidden';
127
- redirectInput.name = 'redirect_cleanly';
128
- redirectInput.value = 'true';
129
- form.appendChild(redirectInput);
130
- #{' '}
131
- // Add selected IDs as hidden inputs
132
- selectedIds.forEach(id => {
133
- const input = document.createElement('input');
134
- input.type = 'hidden';
135
- input.name = 'job_ids[]';
136
- input.value = id;
137
- form.appendChild(input);
138
- });
139
- #{' '}
140
- // Submit the form and then replace the URL location immediately after
141
- form.submit();
142
- #{' '}
143
- // Delay the redirect to give the form time to submit
144
- setTimeout(function() {
145
- // Reset to the clean URL without query parameters
146
- window.history.replaceState({}, '', window.location.pathname);
147
- }, 100);
148
- }
149
- });
150
- #{' '}
151
- discardSelectedBtn.addEventListener('click', function() {
152
- const selectedIds = Array.from(document.querySelectorAll('.job-checkbox:checked')).map(cb => cb.value);
153
- if (selectedIds.length === 0) return;
154
- #{' '}
155
- if (confirm('Are you sure you want to discard the selected jobs?')) {
156
- form.action = '#{discard_failed_jobs_path}';
157
- #{' '}
158
- // Add a special flag to indicate this should redirect properly
159
- const redirectInput = document.createElement('input');
160
- redirectInput.type = 'hidden';
161
- redirectInput.name = 'redirect_cleanly';
162
- redirectInput.value = 'true';
163
- form.appendChild(redirectInput);
164
- #{' '}
165
- // Add selected IDs as hidden inputs
166
- selectedIds.forEach(id => {
167
- const input = document.createElement('input');
168
- input.type = 'hidden';
169
- input.name = 'job_ids[]';
170
- input.value = id;
171
- form.appendChild(input);
172
- });
173
- #{' '}
174
- // Submit the form and then replace the URL location immediately after
175
- form.submit();
176
- #{' '}
177
- // Delay the redirect to give the form time to submit
178
- setTimeout(function() {
179
- // Reset to the clean URL without query parameters
180
- window.history.replaceState({}, '', window.location.pathname);
181
- }, 100);
182
- }
183
- });
184
- #{' '}
185
- // Initialize button state
186
- updateButtonState();
187
- #{' '}
188
- // Global function for retry action
189
- window.submitRetryForm = function(id) {
190
- const form = document.createElement('form');
191
- form.method = 'post';
192
- form.action = '#{retry_failed_job_path(id: 'PLACEHOLDER')}';
193
- form.action = form.action.replace('PLACEHOLDER', id);
194
- form.style.display = 'none';
195
- #{' '}
196
- // Add a special flag to indicate this should redirect properly
197
- const redirectInput = document.createElement('input');
198
- redirectInput.type = 'hidden';
199
- redirectInput.name = 'redirect_cleanly';
200
- redirectInput.value = 'true';
201
- form.appendChild(redirectInput);
202
- #{' '}
203
- document.body.appendChild(form);
204
- #{' '}
205
- // Submit the form and then replace the URL location immediately after
206
- form.submit();
207
- #{' '}
208
- // Delay the redirect to give the form time to submit
209
- setTimeout(function() {
210
- // Reset to the clean URL without query parameters
211
- window.history.replaceState({}, '', window.location.pathname);
212
- }, 100);
213
- };
214
- #{' '}
215
- // Global function for discard action
216
- window.submitDiscardForm = function(id) {
217
- if (confirm('Are you sure you want to discard this job?')) {
218
- const form = document.createElement('form');
219
- form.method = 'post';
220
- form.action = '#{discard_failed_job_path(id: 'PLACEHOLDER')}';
221
- form.action = form.action.replace('PLACEHOLDER', id);
222
- form.style.display = 'none';
223
- #{' '}
224
- // Add a special flag to indicate this should redirect properly
225
- const redirectInput = document.createElement('input');
226
- redirectInput.type = 'hidden';
227
- redirectInput.name = 'redirect_cleanly';
228
- redirectInput.value = 'true';
229
- form.appendChild(redirectInput);
230
- #{' '}
231
- document.body.appendChild(form);
232
- #{' '}
233
- // Submit the form and then replace the URL location immediately after
234
- form.submit();
235
- #{' '}
236
- // Delay the redirect to give the form time to submit
237
- setTimeout(function() {
238
- // Reset to the clean URL without query parameters
239
- window.history.replaceState({}, '', window.location.pathname);
240
- }, 100);
241
- }
242
- };
243
- });
244
- </script>
245
- HTML
246
- end
247
-
248
- def generate_row(failed_execution)
249
- job = failed_execution.job
250
- error = parse_error(failed_execution.error)
251
-
252
- <<-HTML
253
- <tr>
254
- <td><input type="checkbox" class="job-checkbox" value="#{failed_execution.id}"></td>
255
- <td>
256
- <div class="job-class"><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></div>
257
- <div class="job-meta">
258
- <span class="job-timestamp">Queued at: #{format_datetime(job.created_at)}</span>
259
- </div>
260
- </td>
261
- <td>
262
- <div class="job-queue">#{queue_link(job.queue_name)}</div>
263
- </td>
264
- <td>
265
- <div class="error-message">#{error[:message].to_s.truncate(100)}</div>
266
- </td>
267
- <td>#{format_arguments(job.arguments)}</td>
268
- <td>#{format_datetime(failed_execution.created_at)}</td>
269
- <td class="actions-cell">
270
- <div class="job-actions">
271
- <a href="javascript:void(0)"#{' '}
272
- onclick="submitRetryForm(#{failed_execution.id})"#{' '}
273
- class="action-button retry-button">Retry</a>
274
- #{' '}
275
- <a href="javascript:void(0)"#{' '}
276
- onclick="submitDiscardForm(#{failed_execution.id})"#{' '}
277
- class="action-button discard-button">Discard</a>
278
- </div>
279
- </td>
280
- </tr>
281
- HTML
282
- end
283
-
284
- def parse_error(error)
285
- return { message: 'Unknown error', backtrace: '' } unless error
286
-
287
- if error.is_a?(String)
288
- { message: error, backtrace: '' }
289
- elsif error.is_a?(Hash)
290
- message = error['message'] || error[:message] || 'Unknown error'
291
- backtrace = error['backtrace'] || error[:backtrace] || []
292
- backtrace = backtrace.join("\n") if backtrace.is_a?(Array)
293
- { message: message, backtrace: backtrace }
294
- else
295
- { message: 'Unknown error format', backtrace: error.to_s }
296
- end
297
- end
298
-
299
- def get_queue_name(failed_execution, job)
300
- # Try to get queue_name from failed_execution if the method exists
301
- if failed_execution.respond_to?(:queue_name) && !failed_execution.queue_name.nil?
302
- failed_execution.queue_name
303
- else
304
- # Fall back to job's queue_name
305
- job.queue_name
306
- end
307
- rescue NoMethodError
308
- # If there's an error accessing queue_name, fall back to job's queue_name
309
- job.queue_name
310
- end
311
- end
312
- end
@@ -1,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidQueueMonitor
4
- class InProgressJobsPresenter < BasePresenter
5
- include SolidQueueMonitor::Engine.routes.url_helpers
6
-
7
- def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
8
- @jobs = jobs
9
- @current_page = current_page
10
- @total_pages = total_pages
11
- @filters = filters
12
- @sort = sort
13
- end
14
-
15
- def render
16
- section_wrapper('In Progress Jobs',
17
- generate_filter_form + generate_table + generate_pagination(@current_page, @total_pages))
18
- end
19
-
20
- private
21
-
22
- def generate_filter_form
23
- <<-HTML
24
- <div class="filter-form-container">
25
- <form method="get" action="#{in_progress_jobs_path}" class="filter-form">
26
- <div class="filter-group">
27
- <label for="class_name">Job Class:</label>
28
- <input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
29
- </div>
30
-
31
- <div class="filter-group">
32
- <label for="arguments">Arguments:</label>
33
- <input type="text" name="arguments" id="arguments" value="#{@filters[:arguments]}" placeholder="Filter by arguments">
34
- </div>
35
-
36
- <div class="filter-actions">
37
- <button type="submit" class="filter-button">Apply Filters</button>
38
- <a href="#{in_progress_jobs_path}" class="reset-button">Reset</a>
39
- </div>
40
- </form>
41
- </div>
42
- HTML
43
- end
44
-
45
- def generate_table
46
- <<-HTML
47
- <div class="table-container">
48
- <table>
49
- <thead>
50
- <tr>
51
- #{sortable_header('class_name', 'Job')}
52
- #{sortable_header('queue_name', 'Queue')}
53
- <th>Arguments</th>
54
- #{sortable_header('created_at', 'Started At')}
55
- <th>Process ID</th>
56
- </tr>
57
- </thead>
58
- <tbody>
59
- #{@jobs.map { |execution| generate_row(execution) }.join}
60
- </tbody>
61
- </table>
62
- </div>
63
- HTML
64
- end
65
-
66
- def generate_row(execution)
67
- job = execution.job
68
- <<-HTML
69
- <tr>
70
- <td>
71
- <div class="job-class"><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></div>
72
- <div class="job-meta">
73
- <span class="job-timestamp">Queued at: #{format_datetime(job.created_at)}</span>
74
- </div>
75
- </td>
76
- <td>#{queue_link(job.queue_name)}</td>
77
- <td>#{format_arguments(job.arguments)}</td>
78
- <td>#{format_datetime(execution.created_at)}</td>
79
- <td>#{execution.process_id}</td>
80
- </tr>
81
- HTML
82
- end
83
- end
84
- end