solid_queue_monitor 1.3.0 → 2.1.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 +66 -4
  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} +23 -12
  5. data/app/controllers/solid_queue_monitor/application_controller.rb +9 -3
  6. data/app/controllers/solid_queue_monitor/assets_controller.rb +52 -0
  7. data/app/controllers/solid_queue_monitor/base_controller.rb +0 -29
  8. data/app/controllers/solid_queue_monitor/failed_jobs_controller.rb +3 -7
  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 -7
  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 -7
  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/views/layouts/solid_queue_monitor/application.html.erb +25 -0
  27. data/app/views/solid_queue_monitor/failed_jobs/_row.html.erb +26 -0
  28. data/app/views/solid_queue_monitor/failed_jobs/index.html.erb +38 -0
  29. data/app/views/solid_queue_monitor/in_progress_jobs/_row.html.erb +13 -0
  30. data/app/views/solid_queue_monitor/in_progress_jobs/index.html.erb +25 -0
  31. data/app/views/solid_queue_monitor/jobs/_arguments.html.erb +9 -0
  32. data/app/views/solid_queue_monitor/jobs/_error.html.erb +26 -0
  33. data/app/views/solid_queue_monitor/jobs/_execution_history.html.erb +25 -0
  34. data/app/views/solid_queue_monitor/jobs/_header.html.erb +37 -0
  35. data/app/views/solid_queue_monitor/jobs/_metadata.html.erb +22 -0
  36. data/app/views/solid_queue_monitor/jobs/_raw_data.html.erb +11 -0
  37. data/app/views/solid_queue_monitor/jobs/_timeline.html.erb +29 -0
  38. data/app/views/solid_queue_monitor/jobs/_timing.html.erb +9 -0
  39. data/app/views/solid_queue_monitor/jobs/_worker.html.erb +12 -0
  40. data/app/views/solid_queue_monitor/jobs/show.html.erb +22 -0
  41. data/app/views/solid_queue_monitor/overview/_chart.html.erb +1 -0
  42. data/app/views/solid_queue_monitor/overview/_recent_job_row.html.erb +26 -0
  43. data/app/views/solid_queue_monitor/overview/_recent_jobs.html.erb +31 -0
  44. data/app/views/solid_queue_monitor/overview/_stat_card.html.erb +4 -0
  45. data/app/views/solid_queue_monitor/overview/_stats.html.erb +11 -0
  46. data/app/views/solid_queue_monitor/overview/index.html.erb +9 -0
  47. data/app/views/solid_queue_monitor/queues/_job_row.html.erb +26 -0
  48. data/app/views/solid_queue_monitor/queues/_row.html.erb +33 -0
  49. data/app/views/solid_queue_monitor/queues/index.html.erb +18 -0
  50. data/app/views/solid_queue_monitor/queues/show.html.erb +63 -0
  51. data/app/views/solid_queue_monitor/ready_jobs/_row.html.erb +7 -0
  52. data/app/views/solid_queue_monitor/ready_jobs/index.html.erb +25 -0
  53. data/app/views/solid_queue_monitor/recurring_jobs/_row.html.erb +8 -0
  54. data/app/views/solid_queue_monitor/recurring_jobs/index.html.erb +26 -0
  55. data/app/views/solid_queue_monitor/scheduled_jobs/_row.html.erb +7 -0
  56. data/app/views/solid_queue_monitor/scheduled_jobs/index.html.erb +36 -0
  57. data/app/views/solid_queue_monitor/search/_completed_row.html.erb +6 -0
  58. data/app/views/solid_queue_monitor/search/_failed_row.html.erb +6 -0
  59. data/app/views/solid_queue_monitor/search/_job_row.html.erb +9 -0
  60. data/app/views/solid_queue_monitor/search/_recurring_row.html.erb +6 -0
  61. data/app/views/solid_queue_monitor/search/_section.html.erb +25 -0
  62. data/app/views/solid_queue_monitor/search/index.html.erb +23 -0
  63. data/app/views/solid_queue_monitor/shared/_filters.html.erb +48 -0
  64. data/app/views/solid_queue_monitor/shared/_flash.html.erb +17 -0
  65. data/app/views/solid_queue_monitor/shared/_footer.html.erb +3 -0
  66. data/app/views/solid_queue_monitor/shared/_header.html.erb +81 -0
  67. data/app/views/solid_queue_monitor/shared/_jobs_table.html.erb +20 -0
  68. data/app/views/solid_queue_monitor/shared/_pagination.html.erb +25 -0
  69. data/app/views/solid_queue_monitor/workers/_row.html.erb +22 -0
  70. data/app/views/solid_queue_monitor/workers/index.html.erb +82 -0
  71. data/config/routes.rb +6 -1
  72. data/lib/solid_queue_monitor/engine.rb +2 -0
  73. data/lib/solid_queue_monitor/version.rb +1 -1
  74. data/lib/solid_queue_monitor.rb +8 -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 -225
  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 -707
  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 -178
  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 -325
  89. data/app/services/solid_queue_monitor/chart_presenter.rb +0 -239
  90. data/app/services/solid_queue_monitor/html_generator.rb +0 -427
@@ -1,225 +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: {}, nonce: nil)
9
- @jobs = jobs
10
- @current_page = current_page
11
- @total_pages = total_pages
12
- @filters = filters
13
- @sort = sort
14
- @nonce = nonce
15
- end
16
-
17
- def render
18
- section_wrapper('Failed Jobs',
19
- generate_filter_form + generate_table + generate_pagination(@current_page, @total_pages))
20
- end
21
-
22
- private
23
-
24
- def script_tag_open
25
- @nonce ? %(<script nonce="#{@nonce}">) : '<script>'
26
- end
27
-
28
- def generate_filter_form
29
- <<-HTML
30
- <div class="filter-form-container">
31
- <form method="get" action="#{failed_jobs_path}" class="filter-form">
32
- <div class="filter-group">
33
- <label for="class_name">Job Class:</label>
34
- <input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
35
- </div>
36
-
37
- <div class="filter-group">
38
- <label for="queue_name">Queue:</label>
39
- <input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
40
- </div>
41
-
42
- <div class="filter-group">
43
- <label for="arguments">Arguments:</label>
44
- <input type="text" name="arguments" id="arguments" value="#{@filters[:arguments]}" placeholder="Filter by arguments">
45
- </div>
46
-
47
- <div class="filter-actions">
48
- <button type="submit" class="filter-button">Apply Filters</button>
49
- <a href="#{failed_jobs_path}" class="reset-button">Reset</a>
50
- </div>
51
- </form>
52
- </div>
53
-
54
- <div class="bulk-actions-bar">
55
- <button type="button" class="action-button retry-button" id="retry-selected-top" disabled>Retry Selected</button>
56
- <button type="button" class="action-button discard-button" id="discard-selected-top" disabled>Discard Selected</button>
57
- </div>
58
- HTML
59
- end
60
-
61
- def generate_table
62
- <<-HTML
63
- <form method="post" id="failed-jobs-form">
64
- <div class="table-container">
65
- <table>
66
- <thead>
67
- <tr>
68
- <th><input type="checkbox" id="select-all" class="select-all-checkbox"></th>
69
- #{sortable_header('class_name', 'Job')}
70
- #{sortable_header('queue_name', 'Queue')}
71
- <th>Error</th>
72
- <th>Arguments</th>
73
- #{sortable_header('created_at', 'Failed At')}
74
- <th>Actions</th>
75
- </tr>
76
- </thead>
77
- <tbody>
78
- #{@jobs.map { |failed_execution| generate_row(failed_execution) }.join}
79
- </tbody>
80
- </table>
81
- </div>
82
- </form>
83
-
84
- #{script_tag_open}
85
- document.addEventListener('DOMContentLoaded', function() {
86
- var selectAllHeader = document.getElementById('select-all');
87
- var checkboxes = document.querySelectorAll('.job-checkbox');
88
- var retrySelectedBtn = document.getElementById('retry-selected-top');
89
- var discardSelectedBtn = document.getElementById('discard-selected-top');
90
- var form = document.getElementById('failed-jobs-form');
91
-
92
- function updateButtonState() {
93
- var checkedBoxes = document.querySelectorAll('.job-checkbox:checked');
94
- retrySelectedBtn.disabled = checkedBoxes.length === 0;
95
- discardSelectedBtn.disabled = checkedBoxes.length === 0;
96
- }
97
-
98
- selectAllHeader.addEventListener('change', function() {
99
- checkboxes.forEach(function(cb) { cb.checked = selectAllHeader.checked; });
100
- updateButtonState();
101
- });
102
-
103
- checkboxes.forEach(function(cb) {
104
- cb.addEventListener('change', function() {
105
- updateButtonState();
106
- var allChecked = document.querySelectorAll('.job-checkbox:checked').length === checkboxes.length;
107
- selectAllHeader.checked = allChecked;
108
- });
109
- });
110
-
111
- function bulkSubmit(action, promptMsg) {
112
- var ids = Array.from(document.querySelectorAll('.job-checkbox:checked')).map(function(cb) { return cb.value; });
113
- if (ids.length === 0) return;
114
- if (!confirm(promptMsg)) return;
115
- form.action = action;
116
- appendHidden(form, 'redirect_cleanly', 'true');
117
- ids.forEach(function(id) { appendHidden(form, 'job_ids[]', id); });
118
- form.submit();
119
- setTimeout(function() { window.history.replaceState({}, '', window.location.pathname); }, 100);
120
- }
121
-
122
- function appendHidden(f, name, value) {
123
- var input = document.createElement('input');
124
- input.type = 'hidden';
125
- input.name = name;
126
- input.value = value;
127
- f.appendChild(input);
128
- }
129
-
130
- retrySelectedBtn.addEventListener('click', function() {
131
- bulkSubmit('#{retry_failed_jobs_path}', 'Are you sure you want to retry the selected jobs?');
132
- });
133
- discardSelectedBtn.addEventListener('click', function() {
134
- bulkSubmit('#{discard_failed_jobs_path}', 'Are you sure you want to discard the selected jobs?');
135
- });
136
-
137
- function submitRowAction(action, id) {
138
- var f = document.createElement('form');
139
- f.method = 'post';
140
- f.action = action.replace('PLACEHOLDER', id);
141
- appendHidden(f, 'redirect_cleanly', 'true');
142
- document.body.appendChild(f);
143
- f.submit();
144
- setTimeout(function() { window.history.replaceState({}, '', window.location.pathname); }, 100);
145
- }
146
-
147
- document.addEventListener('click', function(e) {
148
- var btn = e.target.closest('[data-action]');
149
- if (!btn) return;
150
- var id = btn.dataset.jobId;
151
- if (btn.dataset.action === 'retry-failed-job') {
152
- submitRowAction('#{retry_failed_job_path(id: 'PLACEHOLDER')}', id);
153
- } else if (btn.dataset.action === 'discard-failed-job') {
154
- if (confirm('Are you sure you want to discard this job?')) {
155
- submitRowAction('#{discard_failed_job_path(id: 'PLACEHOLDER')}', id);
156
- }
157
- }
158
- });
159
-
160
- updateButtonState();
161
- });
162
- </script>
163
- HTML
164
- end
165
-
166
- def generate_row(failed_execution)
167
- job = failed_execution.job
168
- error = parse_error(failed_execution.error)
169
-
170
- <<-HTML
171
- <tr>
172
- <td><input type="checkbox" class="job-checkbox" value="#{failed_execution.id}"></td>
173
- <td>
174
- <div class="job-class"><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></div>
175
- <div class="job-meta">
176
- <span class="job-timestamp">Queued at: #{format_datetime(job.created_at)}</span>
177
- </div>
178
- </td>
179
- <td>
180
- <div class="job-queue">#{queue_link(job.queue_name)}</div>
181
- </td>
182
- <td>
183
- <div class="error-message">#{error[:message].to_s.truncate(100)}</div>
184
- </td>
185
- <td>#{format_arguments(job.arguments)}</td>
186
- <td>#{format_datetime(failed_execution.created_at)}</td>
187
- <td class="actions-cell">
188
- <div class="job-actions">
189
- <button type="button" data-action="retry-failed-job" data-job-id="#{failed_execution.id}" class="action-button retry-button">Retry</button>
190
- <button type="button" data-action="discard-failed-job" data-job-id="#{failed_execution.id}" class="action-button discard-button">Discard</button>
191
- </div>
192
- </td>
193
- </tr>
194
- HTML
195
- end
196
-
197
- def parse_error(error)
198
- return { message: 'Unknown error', backtrace: '' } unless error
199
-
200
- if error.is_a?(String)
201
- { message: error, backtrace: '' }
202
- elsif error.is_a?(Hash)
203
- message = error['message'] || error[:message] || 'Unknown error'
204
- backtrace = error['backtrace'] || error[:backtrace] || []
205
- backtrace = backtrace.join("\n") if backtrace.is_a?(Array)
206
- { message: message, backtrace: backtrace }
207
- else
208
- { message: 'Unknown error format', backtrace: error.to_s }
209
- end
210
- end
211
-
212
- def get_queue_name(failed_execution, job)
213
- # Try to get queue_name from failed_execution if the method exists
214
- if failed_execution.respond_to?(:queue_name) && !failed_execution.queue_name.nil?
215
- failed_execution.queue_name
216
- else
217
- # Fall back to job's queue_name
218
- job.queue_name
219
- end
220
- rescue NoMethodError
221
- # If there's an error accessing queue_name, fall back to job's queue_name
222
- job.queue_name
223
- end
224
- end
225
- 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