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,173 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidQueueMonitor
4
- class ScheduledJobsPresenter < 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('Scheduled Jobs', generate_filter_form + generate_table_with_actions)
18
- end
19
-
20
- private
21
-
22
- def generate_filter_form
23
- <<-HTML
24
- <div class="filter-form-container">
25
- <form method="get" action="" 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="queue_name">Queue:</label>
33
- <input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
34
- </div>
35
-
36
- <div class="filter-group">
37
- <label for="arguments">Arguments:</label>
38
- <input type="text" name="arguments" id="arguments" value="#{@filters[:arguments]}" placeholder="Filter by arguments">
39
- </div>
40
-
41
- <div class="filter-actions">
42
- <button type="submit" class="filter-button">Apply Filters</button>
43
- <a href="#{scheduled_jobs_path}" class="reset-button">Reset</a>
44
- </div>
45
- </form>
46
- </div>
47
-
48
- <div class="bulk-actions-bar">
49
- <button type="button" class="action-button execute-button" id="execute-selected-top" disabled>Execute Selected</button>
50
- <button type="button" class="action-button discard-button" id="reject-selected-top" disabled>Reject Selected</button>
51
- </div>
52
- HTML
53
- end
54
-
55
- def generate_table_with_actions
56
- <<-HTML
57
- <form id="scheduled-jobs-form" method="POST">
58
- #{generate_table}
59
- </form>
60
- <script>
61
- document.addEventListener('DOMContentLoaded', function() {
62
- const selectAllCheckbox = document.querySelector('th input[type="checkbox"]');
63
- const jobCheckboxes = document.getElementsByName('job_ids[]');
64
- const executeButton = document.getElementById('execute-selected-top');
65
- const rejectButton = document.getElementById('reject-selected-top');
66
- const form = document.getElementById('scheduled-jobs-form');
67
- #{' '}
68
- selectAllCheckbox.addEventListener('change', function() {
69
- jobCheckboxes.forEach(checkbox => checkbox.checked = this.checked);
70
- updateButtonStates();
71
- });
72
-
73
- jobCheckboxes.forEach(checkbox => {
74
- checkbox.addEventListener('change', function() {
75
- selectAllCheckbox.checked = Array.from(jobCheckboxes).every(cb => cb.checked);
76
- updateButtonStates();
77
- });
78
- });
79
- #{' '}
80
- // Add event listener for the execute button
81
- executeButton.addEventListener('click', function() {
82
- const selectedIds = Array.from(document.querySelectorAll('input[name="job_ids[]"]:checked')).map(cb => cb.value);
83
- if (selectedIds.length === 0) return;
84
- #{' '}
85
- submitForm('#{execute_jobs_path}', selectedIds);
86
- });
87
- #{' '}
88
- // Add event listener for the reject button
89
- rejectButton.addEventListener('click', function() {
90
- const selectedIds = Array.from(document.querySelectorAll('input[name="job_ids[]"]:checked')).map(cb => cb.value);
91
- if (selectedIds.length === 0) return;
92
- #{' '}
93
- if (confirm('Are you sure you want to reject the selected jobs? This action cannot be undone.')) {
94
- submitForm('#{reject_jobs_path}', selectedIds);
95
- }
96
- });
97
- #{' '}
98
- function submitForm(actionUrl, selectedIds) {
99
- // Uncheck all checkboxes to prevent duplicate submission
100
- document.querySelectorAll('input[name="job_ids[]"]').forEach(checkbox => {
101
- checkbox.checked = false;
102
- });
103
-
104
- // Clear any existing hidden inputs
105
- document.querySelectorAll('input[type="hidden"][name="job_ids[]"]').forEach(input => input.remove());
106
-
107
- // Set form action
108
- form.action = actionUrl;
109
-
110
- // Add selected IDs as hidden inputs
111
- selectedIds.forEach(id => {
112
- const input = document.createElement('input');
113
- input.type = 'hidden';
114
- input.name = 'job_ids[]';
115
- input.value = id;
116
- form.appendChild(input);
117
- });
118
-
119
- // Submit the form
120
- form.submit();
121
- }
122
- #{' '}
123
- function updateButtonStates() {
124
- const checkboxes = document.getElementsByName('job_ids[]');
125
- const checked = Array.from(checkboxes).some(cb => cb.checked);
126
- executeButton.disabled = !checked;
127
- rejectButton.disabled = !checked;
128
- }
129
- #{' '}
130
- // Initialize button states
131
- updateButtonStates();
132
- });
133
- </script>
134
- HTML
135
- end
136
-
137
- def generate_table
138
- <<-HTML
139
- <div class="table-container">
140
- <table>
141
- <thead>
142
- <tr>
143
- <th width="50"><input type="checkbox"></th>
144
- #{sortable_header('class_name', 'Job')}
145
- #{sortable_header('queue_name', 'Queue')}
146
- #{sortable_header('scheduled_at', 'Scheduled At')}
147
- <th>Arguments</th>
148
- </tr>
149
- </thead>
150
- <tbody>
151
- #{@jobs.map { |execution| generate_row(execution) }.join}
152
- </tbody>
153
- </table>
154
- </div>
155
- #{generate_pagination(@current_page, @total_pages)}
156
- HTML
157
- end
158
-
159
- def generate_row(execution)
160
- <<-HTML
161
- <tr>
162
- <td>
163
- <input type="checkbox" name="job_ids[]" value="#{execution.id}">
164
- </td>
165
- <td><a href="#{job_path(execution.job)}" class="job-class-link">#{execution.job.class_name}</a></td>
166
- <td>#{queue_link(execution.queue_name)}</td>
167
- <td>#{format_datetime(execution.scheduled_at)}</td>
168
- <td>#{format_arguments(execution.job.arguments)}</td>
169
- </tr>
170
- HTML
171
- end
172
- end
173
- end
@@ -1,190 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidQueueMonitor
4
- class SearchResultsPresenter < BasePresenter
5
- def initialize(query, results)
6
- @query = query
7
- @results = results
8
- end
9
-
10
- def render
11
- section_wrapper('Search Results', generate_content)
12
- end
13
-
14
- private
15
-
16
- def generate_content
17
- if @query.blank?
18
- generate_empty_query_message
19
- elsif total_count.zero?
20
- generate_no_results_message
21
- else
22
- generate_results_summary + generate_all_sections
23
- end
24
- end
25
-
26
- def generate_empty_query_message
27
- <<-HTML
28
- <div class="empty-state">
29
- <p>Enter a search term in the header to find jobs across all categories.</p>
30
- </div>
31
- HTML
32
- end
33
-
34
- def generate_no_results_message
35
- <<-HTML
36
- <div class="empty-state">
37
- <p>No results found for "#{escape_html(@query)}"</p>
38
- <p class="results-summary">0 results</p>
39
- </div>
40
- HTML
41
- end
42
-
43
- def generate_results_summary
44
- <<-HTML
45
- <div class="results-summary">
46
- <p>Found #{total_count} #{total_count == 1 ? 'result' : 'results'} for "#{escape_html(@query)}"</p>
47
- </div>
48
- HTML
49
- end
50
-
51
- def generate_all_sections
52
- sections = []
53
- sections << generate_ready_section if @results[:ready].any?
54
- sections << generate_scheduled_section if @results[:scheduled].any?
55
- sections << generate_failed_section if @results[:failed].any?
56
- sections << generate_in_progress_section if @results[:in_progress].any?
57
- sections << generate_completed_section if @results[:completed].any?
58
- sections << generate_recurring_section if @results[:recurring].any?
59
- sections.join
60
- end
61
-
62
- def generate_ready_section
63
- generate_section('Ready Jobs', @results[:ready]) do |execution|
64
- generate_job_row(execution.job, execution.queue_name, execution.created_at)
65
- end
66
- end
67
-
68
- def generate_scheduled_section
69
- generate_section('Scheduled Jobs', @results[:scheduled]) do |execution|
70
- generate_job_row(execution.job, execution.queue_name, execution.scheduled_at, 'Scheduled for')
71
- end
72
- end
73
-
74
- def generate_failed_section
75
- generate_section('Failed Jobs', @results[:failed]) do |execution|
76
- generate_failed_row(execution)
77
- end
78
- end
79
-
80
- def generate_in_progress_section
81
- generate_section('In Progress Jobs', @results[:in_progress]) do |execution|
82
- generate_job_row(execution.job, execution.job.queue_name, execution.created_at, 'Started at')
83
- end
84
- end
85
-
86
- def generate_completed_section
87
- generate_section('Completed Jobs', @results[:completed]) do |job|
88
- generate_completed_row(job)
89
- end
90
- end
91
-
92
- def generate_recurring_section
93
- generate_section('Recurring Tasks', @results[:recurring]) do |task|
94
- generate_recurring_row(task)
95
- end
96
- end
97
-
98
- def generate_section(title, items, &block)
99
- <<-HTML
100
- <div class="search-results-section">
101
- <h3>#{title} (#{items.size})</h3>
102
- <div class="table-container">
103
- <table>
104
- <thead>
105
- <tr>
106
- #{section_headers(title)}
107
- </tr>
108
- </thead>
109
- <tbody>
110
- #{items.map(&block).join}
111
- </tbody>
112
- </table>
113
- </div>
114
- </div>
115
- HTML
116
- end
117
-
118
- def section_headers(title)
119
- case title
120
- when 'Recurring Tasks'
121
- '<th>Key</th><th>Class</th><th>Schedule</th><th>Queue</th>'
122
- when 'Failed Jobs'
123
- '<th>Job</th><th>Queue</th><th>Error</th><th>Failed At</th>'
124
- when 'Completed Jobs'
125
- '<th>Job</th><th>Queue</th><th>Arguments</th><th>Completed At</th>'
126
- else
127
- '<th>Job</th><th>Queue</th><th>Arguments</th><th>Time</th>'
128
- end
129
- end
130
-
131
- def generate_job_row(job, queue_name, time, time_label = 'Created at')
132
- <<-HTML
133
- <tr>
134
- <td><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></td>
135
- <td>#{queue_link(queue_name)}</td>
136
- <td>#{format_arguments(job.arguments)}</td>
137
- <td>
138
- <span class="job-timestamp">#{time_label}: #{format_datetime(time)}</span>
139
- </td>
140
- </tr>
141
- HTML
142
- end
143
-
144
- def generate_failed_row(execution)
145
- job = execution.job
146
- <<-HTML
147
- <tr>
148
- <td><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></td>
149
- <td>#{queue_link(job.queue_name)}</td>
150
- <td><div class="error-message">#{escape_html(execution.error.to_s.truncate(100))}</div></td>
151
- <td>
152
- <span class="job-timestamp">#{format_datetime(execution.created_at)}</span>
153
- </td>
154
- </tr>
155
- HTML
156
- end
157
-
158
- def generate_completed_row(job)
159
- <<-HTML
160
- <tr>
161
- <td><a href="#{job_path(job)}" class="job-class-link">#{job.class_name}</a></td>
162
- <td>#{queue_link(job.queue_name)}</td>
163
- <td>#{format_arguments(job.arguments)}</td>
164
- <td>
165
- <span class="job-timestamp">#{format_datetime(job.finished_at)}</span>
166
- </td>
167
- </tr>
168
- HTML
169
- end
170
-
171
- def generate_recurring_row(task)
172
- <<-HTML
173
- <tr>
174
- <td><strong>#{task.key}</strong></td>
175
- <td>#{task.class_name || '-'}</td>
176
- <td><code>#{task.schedule}</code></td>
177
- <td>#{queue_link(task.queue_name)}</td>
178
- </tr>
179
- HTML
180
- end
181
-
182
- def total_count
183
- @total_count ||= @results.values.sum(&:size)
184
- end
185
-
186
- def escape_html(text)
187
- text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
188
- end
189
- end
190
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidQueueMonitor
4
- class StatsPresenter < BasePresenter
5
- def initialize(stats)
6
- @stats = stats
7
- end
8
-
9
- def render
10
- <<-HTML
11
- <div class="stats-container">
12
- <h3>Queue Statistics</h3>
13
- <div class="stats">
14
- #{generate_stat_card('Active Jobs', @stats[:active_jobs])}
15
- #{generate_stat_card('Ready', @stats[:ready])}
16
- #{generate_stat_card('In Progress', @stats[:in_progress])}
17
- #{generate_stat_card('Scheduled', @stats[:scheduled])}
18
- #{generate_stat_card('Recurring', @stats[:recurring])}
19
- #{generate_stat_card('Failed', @stats[:failed])}
20
- </div>
21
- </div>
22
- HTML
23
- end
24
-
25
- private
26
-
27
- def generate_stat_card(title, value)
28
- <<-HTML
29
- <div class="stat-card">
30
- <h3>#{title}</h3>
31
- <p>#{value}</p>
32
- </div>
33
- HTML
34
- end
35
- end
36
- end