solid_queue_monitor 0.1.0 → 0.1.2
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 +28 -2
- data/app/controllers/solid_queue_monitor/monitor_controller.rb +75 -7
- data/app/presenters/solid_queue_monitor/base_presenter.rb +53 -48
- data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +261 -21
- data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +33 -10
- data/app/services/solid_queue_monitor/failed_job_service.rb +103 -0
- data/app/services/solid_queue_monitor/html_generator.rb +27 -2
- data/app/services/solid_queue_monitor/stylesheet_generator.rb +238 -88
- data/config/routes.rb +12 -6
- data/lib/solid_queue_monitor/version.rb +1 -1
- metadata +3 -2
@@ -37,21 +37,24 @@ module SolidQueueMonitor
|
|
37
37
|
</div>
|
38
38
|
</form>
|
39
39
|
</div>
|
40
|
+
|
41
|
+
<div class="bulk-actions-bar">
|
42
|
+
<button type="button" class="action-button execute-button" id="execute-selected-top" disabled>Execute Selected</button>
|
43
|
+
</div>
|
40
44
|
HTML
|
41
45
|
end
|
42
46
|
|
43
47
|
def generate_table_with_actions
|
44
48
|
<<-HTML
|
45
|
-
<form action="#{execute_jobs_path}" method="POST">
|
49
|
+
<form id="scheduled-jobs-form" action="#{execute_jobs_path}" method="POST">
|
46
50
|
#{generate_table}
|
47
|
-
<div class="table-actions">
|
48
|
-
<button type="submit" class="execute-btn" id="bulk-execute" disabled>Execute Selected</button>
|
49
|
-
</div>
|
50
51
|
</form>
|
51
52
|
<script>
|
52
53
|
document.addEventListener('DOMContentLoaded', function() {
|
53
54
|
const selectAllCheckbox = document.querySelector('th input[type="checkbox"]');
|
54
55
|
const jobCheckboxes = document.getElementsByName('job_ids[]');
|
56
|
+
const executeButton = document.getElementById('execute-selected-top');
|
57
|
+
const form = document.getElementById('scheduled-jobs-form');
|
55
58
|
|
56
59
|
selectAllCheckbox.addEventListener('change', function() {
|
57
60
|
jobCheckboxes.forEach(checkbox => checkbox.checked = this.checked);
|
@@ -64,13 +67,33 @@ module SolidQueueMonitor
|
|
64
67
|
updateExecuteButton();
|
65
68
|
});
|
66
69
|
});
|
70
|
+
|
71
|
+
// Add event listener for the execute button
|
72
|
+
executeButton.addEventListener('click', function() {
|
73
|
+
const selectedIds = Array.from(document.querySelectorAll('input[name="job_ids[]"]:checked')).map(cb => cb.value);
|
74
|
+
if (selectedIds.length === 0) return;
|
75
|
+
|
76
|
+
// Add selected IDs as hidden inputs
|
77
|
+
selectedIds.forEach(id => {
|
78
|
+
const input = document.createElement('input');
|
79
|
+
input.type = 'hidden';
|
80
|
+
input.name = 'job_ids[]';
|
81
|
+
input.value = id;
|
82
|
+
form.appendChild(input);
|
83
|
+
});
|
84
|
+
|
85
|
+
form.submit();
|
86
|
+
});
|
87
|
+
|
88
|
+
function updateExecuteButton() {
|
89
|
+
const checkboxes = document.getElementsByName('job_ids[]');
|
90
|
+
const checked = Array.from(checkboxes).some(cb => cb.checked);
|
91
|
+
executeButton.disabled = !checked;
|
92
|
+
}
|
93
|
+
|
94
|
+
// Initialize button state
|
95
|
+
updateExecuteButton();
|
67
96
|
});
|
68
|
-
|
69
|
-
function updateExecuteButton() {
|
70
|
-
const checkboxes = document.getElementsByName('job_ids[]');
|
71
|
-
const checked = Array.from(checkboxes).some(cb => cb.checked);
|
72
|
-
document.getElementById('bulk-execute').disabled = !checked;
|
73
|
-
}
|
74
97
|
</script>
|
75
98
|
HTML
|
76
99
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class FailedJobService
|
3
|
+
def retry_job(failed_execution_id)
|
4
|
+
failed_execution = SolidQueue::FailedExecution.find_by(id: failed_execution_id)
|
5
|
+
return { success: false, message: "Failed job not found" } unless failed_execution
|
6
|
+
|
7
|
+
job = failed_execution.job
|
8
|
+
return { success: false, message: "Associated job not found" } unless job
|
9
|
+
|
10
|
+
ActiveRecord::Base.transaction do
|
11
|
+
# Create a ready execution for the job
|
12
|
+
SolidQueue::ReadyExecution.create!(
|
13
|
+
job_id: job.id,
|
14
|
+
queue_name: get_queue_name(failed_execution, job),
|
15
|
+
priority: job.priority
|
16
|
+
)
|
17
|
+
|
18
|
+
# Delete the failed execution
|
19
|
+
failed_execution.destroy!
|
20
|
+
end
|
21
|
+
|
22
|
+
{ success: true, message: "Job moved to ready queue for retry" }
|
23
|
+
end
|
24
|
+
|
25
|
+
def discard_job(failed_execution_id)
|
26
|
+
failed_execution = SolidQueue::FailedExecution.find_by(id: failed_execution_id)
|
27
|
+
return { success: false, message: "Failed job not found" } unless failed_execution
|
28
|
+
|
29
|
+
job = failed_execution.job
|
30
|
+
return { success: false, message: "Associated job not found" } unless job
|
31
|
+
|
32
|
+
ActiveRecord::Base.transaction do
|
33
|
+
# Mark the job as finished
|
34
|
+
job.update!(finished_at: Time.current)
|
35
|
+
|
36
|
+
# Delete the failed execution
|
37
|
+
failed_execution.destroy!
|
38
|
+
end
|
39
|
+
|
40
|
+
{ success: true, message: "Job has been discarded" }
|
41
|
+
end
|
42
|
+
|
43
|
+
def retry_all(job_ids)
|
44
|
+
return { success: false, message: "No jobs selected" } if job_ids.blank?
|
45
|
+
|
46
|
+
success_count = 0
|
47
|
+
failed_count = 0
|
48
|
+
|
49
|
+
job_ids.each do |id|
|
50
|
+
result = retry_job(id)
|
51
|
+
if result[:success]
|
52
|
+
success_count += 1
|
53
|
+
else
|
54
|
+
failed_count += 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if success_count > 0 && failed_count == 0
|
59
|
+
{ success: true, message: "All selected jobs have been queued for retry" }
|
60
|
+
elsif success_count > 0 && failed_count > 0
|
61
|
+
{ success: true, message: "#{success_count} jobs queued for retry, #{failed_count} failed" }
|
62
|
+
else
|
63
|
+
{ success: false, message: "Failed to retry jobs" }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def discard_all(job_ids)
|
68
|
+
return { success: false, message: "No jobs selected" } if job_ids.blank?
|
69
|
+
|
70
|
+
success_count = 0
|
71
|
+
failed_count = 0
|
72
|
+
|
73
|
+
job_ids.each do |id|
|
74
|
+
result = discard_job(id)
|
75
|
+
if result[:success]
|
76
|
+
success_count += 1
|
77
|
+
else
|
78
|
+
failed_count += 1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if success_count > 0 && failed_count == 0
|
83
|
+
{ success: true, message: "All selected jobs have been discarded" }
|
84
|
+
elsif success_count > 0 && failed_count > 0
|
85
|
+
{ success: true, message: "#{success_count} jobs discarded, #{failed_count} failed" }
|
86
|
+
else
|
87
|
+
{ success: false, message: "Failed to discard jobs" }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def get_queue_name(failed_execution, job)
|
94
|
+
# Try to get queue_name from failed_execution if the method exists
|
95
|
+
if failed_execution.respond_to?(:queue_name) && failed_execution.queue_name.present?
|
96
|
+
failed_execution.queue_name
|
97
|
+
else
|
98
|
+
# Fall back to job's queue_name
|
99
|
+
job.queue_name
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -18,7 +18,7 @@ module SolidQueueMonitor
|
|
18
18
|
<title>Solid Queue Monitor - #{@title}</title>
|
19
19
|
#{generate_head}
|
20
20
|
</head>
|
21
|
-
<body>
|
21
|
+
<body class="solid_queue_monitor">
|
22
22
|
#{generate_body}
|
23
23
|
</body>
|
24
24
|
</html>
|
@@ -54,7 +54,32 @@ module SolidQueueMonitor
|
|
54
54
|
def render_message
|
55
55
|
return '' unless @message
|
56
56
|
class_name = @message_type == 'success' ? 'message-success' : 'message-error'
|
57
|
-
|
57
|
+
<<-HTML
|
58
|
+
<div id="flash-message" class="message #{class_name}">#{@message}</div>
|
59
|
+
<script>
|
60
|
+
// Automatically hide the flash message after 5 seconds
|
61
|
+
document.addEventListener('DOMContentLoaded', function() {
|
62
|
+
var flashMessage = document.getElementById('flash-message');
|
63
|
+
if (flashMessage) {
|
64
|
+
setTimeout(function() {
|
65
|
+
flashMessage.style.opacity = '1';
|
66
|
+
// Fade out animation
|
67
|
+
var fadeEffect = setInterval(function() {
|
68
|
+
if (!flashMessage.style.opacity) {
|
69
|
+
flashMessage.style.opacity = 1;
|
70
|
+
}
|
71
|
+
if (flashMessage.style.opacity > 0) {
|
72
|
+
flashMessage.style.opacity -= 0.1;
|
73
|
+
} else {
|
74
|
+
clearInterval(fadeEffect);
|
75
|
+
flashMessage.style.display = 'none';
|
76
|
+
}
|
77
|
+
}, 50);
|
78
|
+
}, 5000); // 5 seconds
|
79
|
+
}
|
80
|
+
});
|
81
|
+
</script>
|
82
|
+
HTML
|
58
83
|
end
|
59
84
|
|
60
85
|
def generate_header
|