solid_queue_monitor 0.4.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.
- checksums.yaml +4 -4
- data/README.md +11 -4
- data/app/controllers/solid_queue_monitor/application_controller.rb +11 -2
- data/app/controllers/solid_queue_monitor/base_controller.rb +15 -7
- data/app/controllers/solid_queue_monitor/overview_controller.rb +11 -0
- data/app/controllers/solid_queue_monitor/queues_controller.rb +18 -1
- data/app/presenters/solid_queue_monitor/queues_presenter.rb +45 -6
- data/app/services/solid_queue_monitor/chart_data_service.rb +100 -0
- data/app/services/solid_queue_monitor/chart_presenter.rb +239 -0
- data/app/services/solid_queue_monitor/html_generator.rb +168 -8
- data/app/services/solid_queue_monitor/queue_pause_service.rb +34 -0
- data/app/services/solid_queue_monitor/stylesheet_generator.rb +425 -33
- data/config/database.yml +3 -0
- data/config/routes.rb +9 -1
- data/lib/solid_queue_monitor/engine.rb +5 -0
- data/lib/solid_queue_monitor/version.rb +1 -1
- metadata +5 -1
|
@@ -51,6 +51,7 @@ module SolidQueueMonitor
|
|
|
51
51
|
#{generate_footer}
|
|
52
52
|
</div>
|
|
53
53
|
#{generate_auto_refresh_script}
|
|
54
|
+
#{generate_chart_script}
|
|
54
55
|
HTML
|
|
55
56
|
end
|
|
56
57
|
|
|
@@ -87,20 +88,32 @@ module SolidQueueMonitor
|
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
def generate_header
|
|
91
|
+
nav_items = [
|
|
92
|
+
{ path: root_path, label: 'Overview', match: 'Overview' },
|
|
93
|
+
{ path: ready_jobs_path, label: 'Ready Jobs', match: 'Ready Jobs' },
|
|
94
|
+
{ path: in_progress_jobs_path, label: 'In Progress Jobs', match: 'In Progress' },
|
|
95
|
+
{ path: scheduled_jobs_path, label: 'Scheduled Jobs', match: 'Scheduled Jobs' },
|
|
96
|
+
{ path: recurring_jobs_path, label: 'Recurring Jobs', match: 'Recurring Jobs' },
|
|
97
|
+
{ path: failed_jobs_path, label: 'Failed Jobs', match: 'Failed Jobs' },
|
|
98
|
+
{ path: queues_path, label: 'Queues', match: 'Queues' }
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
nav_links = nav_items.map do |item|
|
|
102
|
+
active_class = @title&.include?(item[:match]) ? 'active' : ''
|
|
103
|
+
"<a href=\"#{item[:path]}\" class=\"nav-link #{active_class}\">#{item[:label]}</a>"
|
|
104
|
+
end.join("\n ")
|
|
105
|
+
|
|
90
106
|
<<-HTML
|
|
91
107
|
<header>
|
|
92
108
|
<div class="header-top">
|
|
93
109
|
<h1>Solid Queue Monitor</h1>
|
|
94
|
-
|
|
110
|
+
<div class="header-controls">
|
|
111
|
+
#{generate_auto_refresh_controls}
|
|
112
|
+
#{generate_theme_toggle}
|
|
113
|
+
</div>
|
|
95
114
|
</div>
|
|
96
115
|
<nav class="navigation">
|
|
97
|
-
|
|
98
|
-
<a href="#{ready_jobs_path}" class="nav-link">Ready Jobs</a>
|
|
99
|
-
<a href="#{in_progress_jobs_path}" class="nav-link">In Progress Jobs</a>
|
|
100
|
-
<a href="#{scheduled_jobs_path}" class="nav-link">Scheduled Jobs</a>
|
|
101
|
-
<a href="#{recurring_jobs_path}" class="nav-link">Recurring Jobs</a>
|
|
102
|
-
<a href="#{failed_jobs_path}" class="nav-link">Failed Jobs</a>
|
|
103
|
-
<a href="#{queues_path}" class="nav-link">Queues</a>
|
|
116
|
+
#{nav_links}
|
|
104
117
|
</nav>
|
|
105
118
|
</header>
|
|
106
119
|
HTML
|
|
@@ -135,6 +148,27 @@ module SolidQueueMonitor
|
|
|
135
148
|
HTML
|
|
136
149
|
end
|
|
137
150
|
|
|
151
|
+
def generate_theme_toggle
|
|
152
|
+
<<-HTML
|
|
153
|
+
<button class="theme-toggle-btn" id="theme-toggle-btn" title="Toggle dark mode">
|
|
154
|
+
<svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
155
|
+
<circle cx="12" cy="12" r="5"></circle>
|
|
156
|
+
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
157
|
+
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
158
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
159
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
160
|
+
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
161
|
+
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
162
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
163
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
164
|
+
</svg>
|
|
165
|
+
<svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
166
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
167
|
+
</svg>
|
|
168
|
+
</button>
|
|
169
|
+
HTML
|
|
170
|
+
end
|
|
171
|
+
|
|
138
172
|
def generate_auto_refresh_script
|
|
139
173
|
return '' unless SolidQueueMonitor.auto_refresh_enabled
|
|
140
174
|
|
|
@@ -212,6 +246,132 @@ module SolidQueueMonitor
|
|
|
212
246
|
JS
|
|
213
247
|
end
|
|
214
248
|
|
|
249
|
+
def generate_chart_script
|
|
250
|
+
<<-HTML
|
|
251
|
+
<script>
|
|
252
|
+
#{theme_toggle_javascript}
|
|
253
|
+
#{chart_tooltip_javascript}
|
|
254
|
+
</script>
|
|
255
|
+
HTML
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def theme_toggle_javascript
|
|
259
|
+
<<-JS
|
|
260
|
+
(function() {
|
|
261
|
+
var body = document.body;
|
|
262
|
+
var themeBtn = document.getElementById('theme-toggle-btn');
|
|
263
|
+
var storageKey = 'sqm_dark_theme';
|
|
264
|
+
|
|
265
|
+
// Check for saved preference or system preference
|
|
266
|
+
function getPreferredTheme() {
|
|
267
|
+
var saved = localStorage.getItem(storageKey);
|
|
268
|
+
if (saved !== null) {
|
|
269
|
+
return saved === 'true';
|
|
270
|
+
}
|
|
271
|
+
// Check system preference
|
|
272
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function setTheme(isDark) {
|
|
276
|
+
if (isDark) {
|
|
277
|
+
body.classList.add('dark-theme');
|
|
278
|
+
} else {
|
|
279
|
+
body.classList.remove('dark-theme');
|
|
280
|
+
}
|
|
281
|
+
localStorage.setItem(storageKey, isDark ? 'true' : 'false');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Initialize theme
|
|
285
|
+
setTheme(getPreferredTheme());
|
|
286
|
+
|
|
287
|
+
// Toggle on button click
|
|
288
|
+
if (themeBtn) {
|
|
289
|
+
themeBtn.addEventListener('click', function() {
|
|
290
|
+
var isDark = body.classList.contains('dark-theme');
|
|
291
|
+
setTheme(!isDark);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Listen for system preference changes
|
|
296
|
+
if (window.matchMedia) {
|
|
297
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
|
298
|
+
// Only auto-switch if user hasn't manually set a preference
|
|
299
|
+
if (localStorage.getItem(storageKey) === null) {
|
|
300
|
+
setTheme(e.matches);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
305
|
+
JS
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def chart_tooltip_javascript
|
|
309
|
+
<<-JS
|
|
310
|
+
(function() {
|
|
311
|
+
// Chart collapse/expand functionality
|
|
312
|
+
var chartSection = document.getElementById('chart-section');
|
|
313
|
+
var toggleBtn = document.getElementById('chart-toggle-btn');
|
|
314
|
+
|
|
315
|
+
if (chartSection && toggleBtn) {
|
|
316
|
+
var isCollapsed = localStorage.getItem('sqm_chart_collapsed') === 'true';
|
|
317
|
+
|
|
318
|
+
if (isCollapsed) {
|
|
319
|
+
chartSection.classList.add('collapsed');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
toggleBtn.addEventListener('click', function() {
|
|
323
|
+
chartSection.classList.toggle('collapsed');
|
|
324
|
+
var collapsed = chartSection.classList.contains('collapsed');
|
|
325
|
+
localStorage.setItem('sqm_chart_collapsed', collapsed ? 'true' : 'false');
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Chart tooltip functionality
|
|
330
|
+
var tooltip = document.getElementById('chart-tooltip');
|
|
331
|
+
if (!tooltip) return;
|
|
332
|
+
|
|
333
|
+
var dataPoints = document.querySelectorAll('.data-point');
|
|
334
|
+
var seriesNames = { created: 'Created', completed: 'Completed', failed: 'Failed' };
|
|
335
|
+
|
|
336
|
+
dataPoints.forEach(function(point) {
|
|
337
|
+
point.addEventListener('mouseenter', function(e) {
|
|
338
|
+
var series = this.getAttribute('data-series');
|
|
339
|
+
var label = this.getAttribute('data-label');
|
|
340
|
+
var value = this.getAttribute('data-value');
|
|
341
|
+
|
|
342
|
+
tooltip.querySelector('.tooltip-label').textContent = label;
|
|
343
|
+
tooltip.querySelector('.tooltip-value').textContent = seriesNames[series] + ': ' + value;
|
|
344
|
+
tooltip.style.display = 'block';
|
|
345
|
+
positionTooltip(e);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
point.addEventListener('mousemove', function(e) {
|
|
349
|
+
positionTooltip(e);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
point.addEventListener('mouseleave', function() {
|
|
353
|
+
tooltip.style.display = 'none';
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
function positionTooltip(e) {
|
|
358
|
+
var x = e.clientX + 10;
|
|
359
|
+
var y = e.clientY - 30;
|
|
360
|
+
|
|
361
|
+
if (x + tooltip.offsetWidth > window.innerWidth) {
|
|
362
|
+
x = e.clientX - tooltip.offsetWidth - 10;
|
|
363
|
+
}
|
|
364
|
+
if (y < 0) {
|
|
365
|
+
y = e.clientY + 10;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
tooltip.style.left = x + 'px';
|
|
369
|
+
tooltip.style.top = y + 'px';
|
|
370
|
+
}
|
|
371
|
+
})();
|
|
372
|
+
JS
|
|
373
|
+
end
|
|
374
|
+
|
|
215
375
|
def default_url_options
|
|
216
376
|
{ only_path: true }
|
|
217
377
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueMonitor
|
|
4
|
+
class QueuePauseService
|
|
5
|
+
delegate :paused?, to: :@queue
|
|
6
|
+
|
|
7
|
+
def initialize(queue_name)
|
|
8
|
+
@queue_name = queue_name
|
|
9
|
+
@queue = SolidQueue::Queue.new(queue_name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pause
|
|
13
|
+
return { success: false, message: "Queue '#{@queue_name}' is already paused" } if paused?
|
|
14
|
+
|
|
15
|
+
@queue.pause
|
|
16
|
+
{ success: true, message: "Queue '#{@queue_name}' has been paused" }
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
{ success: false, message: "Failed to pause queue: #{e.message}" }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resume
|
|
22
|
+
return { success: false, message: "Queue '#{@queue_name}' is not paused" } unless paused?
|
|
23
|
+
|
|
24
|
+
@queue.resume
|
|
25
|
+
{ success: true, message: "Queue '#{@queue_name}' has been resumed" }
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
{ success: false, message: "Failed to resume queue: #{e.message}" }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.paused_queues
|
|
31
|
+
SolidQueue::Pause.pluck(:queue_name)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|