solid_queue_monitor 0.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.
- checksums.yaml +7 -0
- data/README.md +128 -0
- data/Rakefile +23 -0
- data/app/controllers/solid_queue_monitor/monitor_controller.rb +250 -0
- data/app/presenters/solid_queue_monitor/base_presenter.rb +136 -0
- data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +70 -0
- data/app/presenters/solid_queue_monitor/jobs_presenter.rb +100 -0
- data/app/presenters/solid_queue_monitor/queues_presenter.rb +62 -0
- data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +72 -0
- data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +77 -0
- data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +114 -0
- data/app/presenters/solid_queue_monitor/stats_presenter.rb +35 -0
- data/app/services/solid_queue_monitor/authentication_service.rb +14 -0
- data/app/services/solid_queue_monitor/execute_job_service.rb +28 -0
- data/app/services/solid_queue_monitor/html_generator.rb +88 -0
- data/app/services/solid_queue_monitor/pagination_service.rb +31 -0
- data/app/services/solid_queue_monitor/stats_calculator.rb +15 -0
- data/app/services/solid_queue_monitor/status_calculator.rb +14 -0
- data/app/services/solid_queue_monitor/stylesheet_generator.rb +395 -0
- data/config/initializers/solid_queue_monitor.rb +5 -0
- data/config/routes.rb +11 -0
- data/lib/generators/solid_queue_monitor/install_generator.rb +23 -0
- data/lib/generators/solid_queue_monitor/templates/README.md +23 -0
- data/lib/generators/solid_queue_monitor/templates/initializer.rb +14 -0
- data/lib/solid_queue_monitor/engine.rb +14 -0
- data/lib/solid_queue_monitor/version.rb +5 -0
- data/lib/solid_queue_monitor.rb +24 -0
- data/lib/tasks/app.rake +135 -0
- metadata +240 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class JobsPresenter < BasePresenter
|
3
|
+
include Rails.application.routes.url_helpers
|
4
|
+
include SolidQueueMonitor::Engine.routes.url_helpers
|
5
|
+
|
6
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
7
|
+
@jobs = jobs
|
8
|
+
@current_page = current_page
|
9
|
+
@total_pages = total_pages
|
10
|
+
@filters = filters
|
11
|
+
end
|
12
|
+
|
13
|
+
def render
|
14
|
+
<<-HTML
|
15
|
+
<div class="section-wrapper">
|
16
|
+
<div class="section">
|
17
|
+
<h3>Recent Jobs</h3>
|
18
|
+
#{generate_filter_form}
|
19
|
+
#{generate_table}
|
20
|
+
#{generate_pagination(@current_page, @total_pages)}
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
HTML
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def generate_filter_form
|
29
|
+
<<-HTML
|
30
|
+
<div class="filter-form-container">
|
31
|
+
<form method="get" action="" 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="status">Status:</label>
|
44
|
+
<select name="status" id="status">
|
45
|
+
<option value="">All Statuses</option>
|
46
|
+
<option value="completed" #{@filters[:status] == 'completed' ? 'selected' : ''}>Completed</option>
|
47
|
+
<option value="failed" #{@filters[:status] == 'failed' ? 'selected' : ''}>Failed</option>
|
48
|
+
<option value="scheduled" #{@filters[:status] == 'scheduled' ? 'selected' : ''}>Scheduled</option>
|
49
|
+
<option value="pending" #{@filters[:status] == 'pending' ? 'selected' : ''}>Pending</option>
|
50
|
+
</select>
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<div class="filter-actions">
|
54
|
+
<button type="submit" class="filter-button">Apply Filters</button>
|
55
|
+
<a href="#{root_path}" class="reset-button">Reset</a>
|
56
|
+
</div>
|
57
|
+
</form>
|
58
|
+
</div>
|
59
|
+
HTML
|
60
|
+
end
|
61
|
+
|
62
|
+
def generate_table
|
63
|
+
<<-HTML
|
64
|
+
<div class="table-container">
|
65
|
+
<table>
|
66
|
+
<thead>
|
67
|
+
<tr>
|
68
|
+
<th>ID</th>
|
69
|
+
<th>Job</th>
|
70
|
+
<th>Queue</th>
|
71
|
+
<th>Status</th>
|
72
|
+
<th>Created At</th>
|
73
|
+
</tr>
|
74
|
+
</thead>
|
75
|
+
<tbody>
|
76
|
+
#{@jobs.map { |job| generate_row(job) }.join}
|
77
|
+
</tbody>
|
78
|
+
</table>
|
79
|
+
</div>
|
80
|
+
HTML
|
81
|
+
end
|
82
|
+
|
83
|
+
def generate_row(job)
|
84
|
+
status = job_status(job)
|
85
|
+
<<-HTML
|
86
|
+
<tr>
|
87
|
+
<td>#{job.id}</td>
|
88
|
+
<td>#{job.class_name}</td>
|
89
|
+
<td>#{job.queue_name}</td>
|
90
|
+
<td><span class='status-badge status-#{status}'>#{status}</span></td>
|
91
|
+
<td>#{format_datetime(job.created_at)}</td>
|
92
|
+
</tr>
|
93
|
+
HTML
|
94
|
+
end
|
95
|
+
|
96
|
+
def job_status(job)
|
97
|
+
SolidQueueMonitor::StatusCalculator.new(job).calculate
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class QueuesPresenter < BasePresenter
|
3
|
+
def initialize(records)
|
4
|
+
@records = records
|
5
|
+
end
|
6
|
+
|
7
|
+
def render
|
8
|
+
section_wrapper('Queues', generate_table)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def generate_table
|
14
|
+
<<-HTML
|
15
|
+
<div class="table-container">
|
16
|
+
<table>
|
17
|
+
<thead>
|
18
|
+
<tr>
|
19
|
+
<th>Queue Name</th>
|
20
|
+
<th>Total Jobs</th>
|
21
|
+
<th>Ready Jobs</th>
|
22
|
+
<th>Scheduled Jobs</th>
|
23
|
+
<th>Failed Jobs</th>
|
24
|
+
</tr>
|
25
|
+
</thead>
|
26
|
+
<tbody>
|
27
|
+
#{@records.map { |queue| generate_row(queue) }.join}
|
28
|
+
</tbody>
|
29
|
+
</table>
|
30
|
+
</div>
|
31
|
+
HTML
|
32
|
+
end
|
33
|
+
|
34
|
+
def generate_row(queue)
|
35
|
+
<<-HTML
|
36
|
+
<tr>
|
37
|
+
<td>#{queue.queue_name || 'default'}</td>
|
38
|
+
<td>#{queue.job_count}</td>
|
39
|
+
<td>#{ready_jobs_count(queue.queue_name)}</td>
|
40
|
+
<td>#{scheduled_jobs_count(queue.queue_name)}</td>
|
41
|
+
<td>#{failed_jobs_count(queue.queue_name)}</td>
|
42
|
+
</tr>
|
43
|
+
HTML
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def ready_jobs_count(queue_name)
|
49
|
+
SolidQueue::ReadyExecution.where(queue_name: queue_name).count
|
50
|
+
end
|
51
|
+
|
52
|
+
def scheduled_jobs_count(queue_name)
|
53
|
+
SolidQueue::ScheduledExecution.where(queue_name: queue_name).count
|
54
|
+
end
|
55
|
+
|
56
|
+
def failed_jobs_count(queue_name)
|
57
|
+
SolidQueue::FailedExecution.joins(:job)
|
58
|
+
.where(solid_queue_jobs: { queue_name: queue_name })
|
59
|
+
.count
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class ReadyJobsPresenter < BasePresenter
|
3
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
4
|
+
@jobs = jobs
|
5
|
+
@current_page = current_page
|
6
|
+
@total_pages = total_pages
|
7
|
+
@filters = filters
|
8
|
+
end
|
9
|
+
|
10
|
+
def render
|
11
|
+
section_wrapper('Ready Jobs', generate_filter_form + generate_table + generate_pagination(@current_page, @total_pages))
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def generate_filter_form
|
17
|
+
<<-HTML
|
18
|
+
<div class="filter-form-container">
|
19
|
+
<form method="get" action="" class="filter-form">
|
20
|
+
<div class="filter-group">
|
21
|
+
<label for="class_name">Job Class:</label>
|
22
|
+
<input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<div class="filter-group">
|
26
|
+
<label for="queue_name">Queue:</label>
|
27
|
+
<input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
|
28
|
+
</div>
|
29
|
+
|
30
|
+
<div class="filter-actions">
|
31
|
+
<button type="submit" class="filter-button">Apply Filters</button>
|
32
|
+
<a href="#{ready_jobs_path}" class="reset-button">Reset</a>
|
33
|
+
</div>
|
34
|
+
</form>
|
35
|
+
</div>
|
36
|
+
HTML
|
37
|
+
end
|
38
|
+
|
39
|
+
def generate_table
|
40
|
+
<<-HTML
|
41
|
+
<div class="table-container">
|
42
|
+
<table>
|
43
|
+
<thead>
|
44
|
+
<tr>
|
45
|
+
<th>Job</th>
|
46
|
+
<th>Queue</th>
|
47
|
+
<th>Priority</th>
|
48
|
+
<th>Arguments</th>
|
49
|
+
<th>Created At</th>
|
50
|
+
</tr>
|
51
|
+
</thead>
|
52
|
+
<tbody>
|
53
|
+
#{@jobs.map { |execution| generate_row(execution) }.join}
|
54
|
+
</tbody>
|
55
|
+
</table>
|
56
|
+
</div>
|
57
|
+
HTML
|
58
|
+
end
|
59
|
+
|
60
|
+
def generate_row(execution)
|
61
|
+
<<-HTML
|
62
|
+
<tr>
|
63
|
+
<td>#{execution.job.class_name}</td>
|
64
|
+
<td>#{execution.queue_name}</td>
|
65
|
+
<td>#{execution.priority}</td>
|
66
|
+
<td>#{format_arguments(execution.job.arguments)}</td>
|
67
|
+
<td>#{format_datetime(execution.created_at)}</td>
|
68
|
+
</tr>
|
69
|
+
HTML
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class RecurringJobsPresenter < BasePresenter
|
3
|
+
include Rails.application.routes.url_helpers
|
4
|
+
include SolidQueueMonitor::Engine.routes.url_helpers
|
5
|
+
|
6
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
7
|
+
@jobs = jobs
|
8
|
+
@current_page = current_page
|
9
|
+
@total_pages = total_pages
|
10
|
+
@filters = filters
|
11
|
+
end
|
12
|
+
|
13
|
+
def render
|
14
|
+
section_wrapper('Recurring Jobs', generate_filter_form + generate_table + generate_pagination(@current_page, @total_pages))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def generate_filter_form
|
20
|
+
<<-HTML
|
21
|
+
<div class="filter-form-container">
|
22
|
+
<form method="get" action="" class="filter-form">
|
23
|
+
<div class="filter-group">
|
24
|
+
<label for="class_name">Job Class:</label>
|
25
|
+
<input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<div class="filter-group">
|
29
|
+
<label for="queue_name">Queue:</label>
|
30
|
+
<input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
|
31
|
+
</div>
|
32
|
+
|
33
|
+
<div class="filter-actions">
|
34
|
+
<button type="submit" class="filter-button">Apply Filters</button>
|
35
|
+
<a href="#{recurring_jobs_path}" class="reset-button">Reset</a>
|
36
|
+
</div>
|
37
|
+
</form>
|
38
|
+
</div>
|
39
|
+
HTML
|
40
|
+
end
|
41
|
+
|
42
|
+
def generate_table
|
43
|
+
<<-HTML
|
44
|
+
<div class="table-container">
|
45
|
+
<table>
|
46
|
+
<thead>
|
47
|
+
<tr>
|
48
|
+
<th>Key</th>
|
49
|
+
<th>Job</th>
|
50
|
+
<th>Schedule</th>
|
51
|
+
<th>Queue</th>
|
52
|
+
<th>Priority</th>
|
53
|
+
<th>Last Updated</th>
|
54
|
+
</tr>
|
55
|
+
</thead>
|
56
|
+
<tbody>
|
57
|
+
#{@jobs.map { |task| generate_row(task) }.join}
|
58
|
+
</tbody>
|
59
|
+
</table>
|
60
|
+
</div>
|
61
|
+
HTML
|
62
|
+
end
|
63
|
+
|
64
|
+
def generate_row(task)
|
65
|
+
<<-HTML
|
66
|
+
<tr>
|
67
|
+
<td>#{task.key}</td>
|
68
|
+
<td>#{task.class_name}</td>
|
69
|
+
<td>#{task.schedule}</td>
|
70
|
+
<td>#{task.queue_name}</td>
|
71
|
+
<td>#{task.priority || 'Default'}</td>
|
72
|
+
<td>#{format_datetime(task.updated_at)}</td>
|
73
|
+
</tr>
|
74
|
+
HTML
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class ScheduledJobsPresenter < BasePresenter
|
3
|
+
|
4
|
+
include Rails.application.routes.url_helpers
|
5
|
+
include SolidQueueMonitor::Engine.routes.url_helpers
|
6
|
+
|
7
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
8
|
+
@jobs = jobs
|
9
|
+
@current_page = current_page
|
10
|
+
@total_pages = total_pages
|
11
|
+
@filters = filters
|
12
|
+
end
|
13
|
+
|
14
|
+
def render
|
15
|
+
section_wrapper('Scheduled Jobs', generate_filter_form + generate_table_with_actions)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def generate_filter_form
|
21
|
+
<<-HTML
|
22
|
+
<div class="filter-form-container">
|
23
|
+
<form method="get" action="" class="filter-form">
|
24
|
+
<div class="filter-group">
|
25
|
+
<label for="class_name">Job Class:</label>
|
26
|
+
<input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<div class="filter-group">
|
30
|
+
<label for="queue_name">Queue:</label>
|
31
|
+
<input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<div class="filter-actions">
|
35
|
+
<button type="submit" class="filter-button">Apply Filters</button>
|
36
|
+
<a href="#{scheduled_jobs_path}" class="reset-button">Reset</a>
|
37
|
+
</div>
|
38
|
+
</form>
|
39
|
+
</div>
|
40
|
+
HTML
|
41
|
+
end
|
42
|
+
|
43
|
+
def generate_table_with_actions
|
44
|
+
<<-HTML
|
45
|
+
<form action="#{execute_jobs_path}" method="POST">
|
46
|
+
#{generate_table}
|
47
|
+
<div class="table-actions">
|
48
|
+
<button type="submit" class="execute-btn" id="bulk-execute" disabled>Execute Selected</button>
|
49
|
+
</div>
|
50
|
+
</form>
|
51
|
+
<script>
|
52
|
+
document.addEventListener('DOMContentLoaded', function() {
|
53
|
+
const selectAllCheckbox = document.querySelector('th input[type="checkbox"]');
|
54
|
+
const jobCheckboxes = document.getElementsByName('job_ids[]');
|
55
|
+
|
56
|
+
selectAllCheckbox.addEventListener('change', function() {
|
57
|
+
jobCheckboxes.forEach(checkbox => checkbox.checked = this.checked);
|
58
|
+
updateExecuteButton();
|
59
|
+
});
|
60
|
+
|
61
|
+
jobCheckboxes.forEach(checkbox => {
|
62
|
+
checkbox.addEventListener('change', function() {
|
63
|
+
selectAllCheckbox.checked = Array.from(jobCheckboxes).every(cb => cb.checked);
|
64
|
+
updateExecuteButton();
|
65
|
+
});
|
66
|
+
});
|
67
|
+
});
|
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
|
+
</script>
|
75
|
+
HTML
|
76
|
+
end
|
77
|
+
|
78
|
+
def generate_table
|
79
|
+
<<-HTML
|
80
|
+
<div class="table-container">
|
81
|
+
<table>
|
82
|
+
<thead>
|
83
|
+
<tr>
|
84
|
+
<th width="50"><input type="checkbox"></th>
|
85
|
+
<th>Job</th>
|
86
|
+
<th>Queue</th>
|
87
|
+
<th>Scheduled At</th>
|
88
|
+
<th>Arguments</th>
|
89
|
+
</tr>
|
90
|
+
</thead>
|
91
|
+
<tbody>
|
92
|
+
#{@jobs.map { |execution| generate_row(execution) }.join}
|
93
|
+
</tbody>
|
94
|
+
</table>
|
95
|
+
</div>
|
96
|
+
#{generate_pagination(@current_page, @total_pages)}
|
97
|
+
HTML
|
98
|
+
end
|
99
|
+
|
100
|
+
def generate_row(execution)
|
101
|
+
<<-HTML
|
102
|
+
<tr>
|
103
|
+
<td>
|
104
|
+
<input type="checkbox" name="job_ids[]" value="#{execution.id}" onchange="updateExecuteButton()">
|
105
|
+
</td>
|
106
|
+
<td>#{execution.job.class_name}</td>
|
107
|
+
<td>#{execution.queue_name}</td>
|
108
|
+
<td>#{format_datetime(execution.scheduled_at)}</td>
|
109
|
+
<td>#{format_arguments(execution.job.arguments)}</td>
|
110
|
+
</tr>
|
111
|
+
HTML
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class StatsPresenter < BasePresenter
|
3
|
+
def initialize(stats)
|
4
|
+
@stats = stats
|
5
|
+
end
|
6
|
+
|
7
|
+
def render
|
8
|
+
<<-HTML
|
9
|
+
<div class="stats-container">
|
10
|
+
<h3>Queue Statistics</h3>
|
11
|
+
<div class="stats">
|
12
|
+
#{generate_stat_card('Total Jobs', @stats[:total_jobs])}
|
13
|
+
#{generate_stat_card('Unique Queues', @stats[:unique_queues])}
|
14
|
+
#{generate_stat_card('Ready', @stats[:ready])}
|
15
|
+
#{generate_stat_card('Scheduled', @stats[:scheduled])}
|
16
|
+
#{generate_stat_card('Failed', @stats[:failed])}
|
17
|
+
#{generate_stat_card('Completed', @stats[:completed])}
|
18
|
+
#{generate_stat_card('Recurring', @stats[:recurring])}
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
HTML
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def generate_stat_card(title, value)
|
27
|
+
<<-HTML
|
28
|
+
<div class="stat-card">
|
29
|
+
<h3>#{title}</h3>
|
30
|
+
<p>#{value}</p>
|
31
|
+
</div>
|
32
|
+
HTML
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class AuthenticationService
|
3
|
+
def self.authenticate(username, password)
|
4
|
+
return true unless SolidQueueMonitor.authentication_enabled
|
5
|
+
|
6
|
+
username == SolidQueueMonitor.username &&
|
7
|
+
password == SolidQueueMonitor.password
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.authentication_required?
|
11
|
+
SolidQueueMonitor.authentication_enabled
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class ExecuteJobService
|
3
|
+
def call(id)
|
4
|
+
execution = SolidQueue::ScheduledExecution.find(id)
|
5
|
+
move_to_ready_queue(execution)
|
6
|
+
end
|
7
|
+
|
8
|
+
def execute_many(ids)
|
9
|
+
SolidQueue::ScheduledExecution.where(id: ids).find_each do |execution|
|
10
|
+
move_to_ready_queue(execution)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def move_to_ready_queue(execution)
|
17
|
+
ActiveRecord::Base.transaction do
|
18
|
+
SolidQueue::ReadyExecution.create!(
|
19
|
+
job: execution.job,
|
20
|
+
queue_name: execution.queue_name,
|
21
|
+
priority: execution.priority
|
22
|
+
)
|
23
|
+
|
24
|
+
execution.destroy
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class HtmlGenerator
|
3
|
+
include Rails.application.routes.url_helpers
|
4
|
+
include SolidQueueMonitor::Engine.routes.url_helpers
|
5
|
+
|
6
|
+
def initialize(title:, content:, message: nil, message_type: nil)
|
7
|
+
@title = title
|
8
|
+
@content = content
|
9
|
+
@message = message
|
10
|
+
@message_type = message_type
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
<<-HTML
|
15
|
+
<!DOCTYPE html>
|
16
|
+
<html>
|
17
|
+
<head>
|
18
|
+
<title>Solid Queue Monitor - #{@title}</title>
|
19
|
+
#{generate_head}
|
20
|
+
</head>
|
21
|
+
<body>
|
22
|
+
#{generate_body}
|
23
|
+
</body>
|
24
|
+
</html>
|
25
|
+
HTML
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def generate_head
|
31
|
+
<<-HTML
|
32
|
+
<meta charset="UTF-8">
|
33
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
34
|
+
<style>
|
35
|
+
#{SolidQueueMonitor::StylesheetGenerator.new.generate}
|
36
|
+
</style>
|
37
|
+
HTML
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_body
|
41
|
+
<<-HTML
|
42
|
+
#{render_message}
|
43
|
+
<div class="container">
|
44
|
+
#{generate_header}
|
45
|
+
<div class="section">
|
46
|
+
<h2>#{@title}</h2>
|
47
|
+
#{@content}
|
48
|
+
</div>
|
49
|
+
#{generate_footer}
|
50
|
+
</div>
|
51
|
+
HTML
|
52
|
+
end
|
53
|
+
|
54
|
+
def render_message
|
55
|
+
return '' unless @message
|
56
|
+
class_name = @message_type == 'success' ? 'message-success' : 'message-error'
|
57
|
+
"<div class='message #{class_name}'>#{@message}</div>"
|
58
|
+
end
|
59
|
+
|
60
|
+
def generate_header
|
61
|
+
<<-HTML
|
62
|
+
<header>
|
63
|
+
<h1>Solid Queue Monitor</h1>
|
64
|
+
<nav class="navigation">
|
65
|
+
<a href="#{root_path}" class="nav-link">Overview</a>
|
66
|
+
<a href="#{ready_jobs_path}" class="nav-link">Ready Jobs</a>
|
67
|
+
<a href="#{recurring_jobs_path}" class="nav-link">Recurring Jobs</a>
|
68
|
+
<a href="#{scheduled_jobs_path}" class="nav-link">Scheduled Jobs</a>
|
69
|
+
<a href="#{failed_jobs_path}" class="nav-link">Failed Jobs</a>
|
70
|
+
<a href="#{queues_path}" class="nav-link">Queues</a>
|
71
|
+
</nav>
|
72
|
+
</header>
|
73
|
+
HTML
|
74
|
+
end
|
75
|
+
|
76
|
+
def generate_footer
|
77
|
+
<<-HTML
|
78
|
+
<footer>
|
79
|
+
<p>Powered by Solid Queue Monitor</p>
|
80
|
+
</footer>
|
81
|
+
HTML
|
82
|
+
end
|
83
|
+
|
84
|
+
def default_url_options
|
85
|
+
{ only_path: true }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class PaginationService
|
3
|
+
def initialize(relation, page, per_page)
|
4
|
+
@relation = relation
|
5
|
+
@page = page
|
6
|
+
@per_page = per_page
|
7
|
+
end
|
8
|
+
|
9
|
+
def paginate
|
10
|
+
{
|
11
|
+
records: paginated_records,
|
12
|
+
total_pages: total_pages,
|
13
|
+
current_page: @page
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def offset
|
20
|
+
(@page - 1) * @per_page
|
21
|
+
end
|
22
|
+
|
23
|
+
def total_pages
|
24
|
+
(@relation.count.to_f / @per_page).ceil
|
25
|
+
end
|
26
|
+
|
27
|
+
def paginated_records
|
28
|
+
@relation.limit(@per_page).offset(offset)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class StatsCalculator
|
3
|
+
def self.calculate
|
4
|
+
{
|
5
|
+
total_jobs: SolidQueue::Job.count,
|
6
|
+
unique_queues: SolidQueue::Job.distinct.count(:queue_name),
|
7
|
+
scheduled: SolidQueue::ScheduledExecution.count,
|
8
|
+
ready: SolidQueue::ReadyExecution.count,
|
9
|
+
failed: SolidQueue::FailedExecution.count,
|
10
|
+
completed: SolidQueue::Job.where.not(finished_at: nil).count,
|
11
|
+
recurring: SolidQueue::RecurringTask.count
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SolidQueueMonitor
|
2
|
+
class StatusCalculator
|
3
|
+
def initialize(job)
|
4
|
+
@job = job
|
5
|
+
end
|
6
|
+
|
7
|
+
def calculate
|
8
|
+
return 'completed' if @job.finished_at.present?
|
9
|
+
return 'failed' if @job.failed?
|
10
|
+
return 'scheduled' if @job.scheduled_at&.future?
|
11
|
+
'pending'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|