solid_queue_monitor 1.0.1 → 1.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 +4 -4
- data/README.md +3 -1
- data/app/controllers/solid_queue_monitor/base_controller.rb +34 -2
- data/app/controllers/solid_queue_monitor/failed_jobs_controller.rb +7 -3
- data/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb +7 -3
- data/app/controllers/solid_queue_monitor/overview_controller.rb +7 -3
- data/app/controllers/solid_queue_monitor/queues_controller.rb +21 -8
- data/app/controllers/solid_queue_monitor/ready_jobs_controller.rb +7 -3
- data/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb +7 -3
- data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +7 -3
- data/app/controllers/solid_queue_monitor/search_controller.rb +12 -0
- data/app/controllers/solid_queue_monitor/workers_controller.rb +7 -4
- data/app/presenters/solid_queue_monitor/base_presenter.rb +47 -5
- data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +6 -6
- data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +5 -4
- data/app/presenters/solid_queue_monitor/jobs_presenter.rb +5 -4
- data/app/presenters/solid_queue_monitor/queue_details_presenter.rb +4 -3
- data/app/presenters/solid_queue_monitor/queues_presenter.rb +4 -3
- data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +6 -5
- data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +6 -5
- data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +5 -4
- data/app/presenters/solid_queue_monitor/search_results_presenter.rb +190 -0
- data/app/presenters/solid_queue_monitor/workers_presenter.rb +4 -3
- data/app/services/solid_queue_monitor/html_generator.rb +23 -2
- data/app/services/solid_queue_monitor/search_service.rb +126 -0
- data/app/services/solid_queue_monitor/stylesheet_generator.rb +118 -0
- data/config/routes.rb +1 -0
- data/lib/solid_queue_monitor/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1014e5b1b9afceff146efc3a81849dd12866578198b8172452d9b6edf42d7786
|
|
4
|
+
data.tar.gz: 0b2c11f5ba47a7e977c93cdb7a048c91d092b9ea97d72af27e908f6f24387fbf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58bf74bb26fbb518d58e555d1a22695d3175b0ea683529685b4cec6322091722fd7a8eb912bb0fbbfa0fce391663777fc69cde137bf4a4cb0fb1e1cf9d16bbf2
|
|
7
|
+
data.tar.gz: 59be98bcc8ee044ac0eac816cab397534c79da53cb98cfb670da1e0747552425f33eaf6dce51e2f906c44f9053b4097a8a79e3f2d6fe30534ddbad6e7f3d3d70
|
data/README.md
CHANGED
|
@@ -35,6 +35,8 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
|
|
|
35
35
|
- **Failed Jobs**: Track and debug failed jobs, with the ability to retry or discard them
|
|
36
36
|
- **Queue Management**: View and filter jobs by queue with pause/resume controls
|
|
37
37
|
- **Pause/Resume Queues**: Temporarily stop processing jobs on specific queues for incident response
|
|
38
|
+
- **Global Search**: Search across all job types by class name, queue, arguments, job ID, and error messages
|
|
39
|
+
- **Sortable Columns**: Click column headers to sort job tables by any column with ascending/descending toggle
|
|
38
40
|
- **Advanced Job Filtering**: Filter jobs by class name, queue, status, and job arguments
|
|
39
41
|
- **Quick Actions**: Retry or discard failed jobs, execute or reject scheduled jobs directly from any view
|
|
40
42
|
- **Performance Optimized**: Designed for high-volume applications with smart pagination
|
|
@@ -70,7 +72,7 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
|
|
|
70
72
|
Add this line to your application's Gemfile:
|
|
71
73
|
|
|
72
74
|
```ruby
|
|
73
|
-
gem 'solid_queue_monitor', '~> 1.
|
|
75
|
+
gem 'solid_queue_monitor', '~> 1.1'
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
Then execute:
|
|
@@ -6,7 +6,7 @@ module SolidQueueMonitor
|
|
|
6
6
|
PaginationService.new(relation, current_page, per_page).paginate
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
def render_page(title, content)
|
|
9
|
+
def render_page(title, content, search_query: nil)
|
|
10
10
|
# Get flash message from instance variable (set by set_flash_message) or session
|
|
11
11
|
message = @flash_message
|
|
12
12
|
message_type = @flash_type
|
|
@@ -27,7 +27,8 @@ module SolidQueueMonitor
|
|
|
27
27
|
title: title,
|
|
28
28
|
content: content,
|
|
29
29
|
message: message,
|
|
30
|
-
message_type: message_type
|
|
30
|
+
message_type: message_type,
|
|
31
|
+
search_query: search_query
|
|
31
32
|
).generate
|
|
32
33
|
|
|
33
34
|
render html: html.html_safe
|
|
@@ -201,5 +202,36 @@ module SolidQueueMonitor
|
|
|
201
202
|
status: params[:status]
|
|
202
203
|
}
|
|
203
204
|
end
|
|
205
|
+
|
|
206
|
+
def sort_params
|
|
207
|
+
{
|
|
208
|
+
sort_by: params[:sort_by],
|
|
209
|
+
sort_direction: params[:sort_direction]
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def apply_sorting(relation, allowed_columns, default_column, default_direction = :desc)
|
|
214
|
+
column = sort_params[:sort_by]
|
|
215
|
+
direction = sort_params[:sort_direction]
|
|
216
|
+
column = default_column unless allowed_columns.include?(column)
|
|
217
|
+
direction = %w[asc desc].include?(direction) ? direction.to_sym : default_direction
|
|
218
|
+
relation.order(column => direction)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def apply_execution_sorting(relation, allowed_columns, default_column, default_direction = :desc)
|
|
222
|
+
column = sort_params[:sort_by]
|
|
223
|
+
direction = sort_params[:sort_direction]
|
|
224
|
+
column = default_column unless allowed_columns.include?(column)
|
|
225
|
+
direction = %w[asc desc].include?(direction) ? direction.to_sym : default_direction
|
|
226
|
+
|
|
227
|
+
# Columns that exist on the jobs table, not on execution tables
|
|
228
|
+
job_table_columns = %w[class_name queue_name]
|
|
229
|
+
|
|
230
|
+
if job_table_columns.include?(column)
|
|
231
|
+
relation.joins(:job).order("solid_queue_jobs.#{column}" => direction)
|
|
232
|
+
else
|
|
233
|
+
relation.order(column => direction)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
204
236
|
end
|
|
205
237
|
end
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class FailedJobsController < BaseController
|
|
5
|
+
SORTABLE_COLUMNS = %w[class_name queue_name created_at].freeze
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
|
-
base_query = SolidQueue::FailedExecution.includes(:job)
|
|
7
|
-
|
|
8
|
+
base_query = SolidQueue::FailedExecution.includes(:job)
|
|
9
|
+
sorted_query = apply_execution_sorting(filter_failed_jobs(base_query), SORTABLE_COLUMNS, 'created_at', :desc)
|
|
10
|
+
@failed_jobs = paginate(sorted_query)
|
|
8
11
|
|
|
9
12
|
render_page('Failed Jobs', SolidQueueMonitor::FailedJobsPresenter.new(@failed_jobs[:records],
|
|
10
13
|
current_page: @failed_jobs[:current_page],
|
|
11
14
|
total_pages: @failed_jobs[:total_pages],
|
|
12
|
-
filters: filter_params
|
|
15
|
+
filters: filter_params,
|
|
16
|
+
sort: sort_params).render)
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
def retry
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class InProgressJobsController < BaseController
|
|
5
|
+
SORTABLE_COLUMNS = %w[class_name queue_name created_at].freeze
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
|
-
base_query = SolidQueue::ClaimedExecution.includes(:job)
|
|
7
|
-
|
|
8
|
+
base_query = SolidQueue::ClaimedExecution.includes(:job)
|
|
9
|
+
sorted_query = apply_execution_sorting(filter_in_progress_jobs(base_query), SORTABLE_COLUMNS, 'created_at', :desc)
|
|
10
|
+
@in_progress_jobs = paginate(sorted_query)
|
|
8
11
|
|
|
9
12
|
render_page('In Progress Jobs', SolidQueueMonitor::InProgressJobsPresenter.new(@in_progress_jobs[:records],
|
|
10
13
|
current_page: @in_progress_jobs[:current_page],
|
|
11
14
|
total_pages: @in_progress_jobs[:total_pages],
|
|
12
|
-
filters: filter_params
|
|
15
|
+
filters: filter_params,
|
|
16
|
+
sort: sort_params).render)
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
private
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class OverviewController < BaseController
|
|
5
|
+
SORTABLE_COLUMNS = %w[class_name queue_name created_at].freeze
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
8
|
@stats = SolidQueueMonitor::StatsCalculator.calculate
|
|
7
9
|
@chart_data = SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate
|
|
8
10
|
|
|
9
|
-
recent_jobs_query = SolidQueue::Job.
|
|
10
|
-
|
|
11
|
+
recent_jobs_query = SolidQueue::Job.limit(100)
|
|
12
|
+
sorted_query = apply_sorting(filter_jobs(recent_jobs_query), SORTABLE_COLUMNS, 'created_at', :desc)
|
|
13
|
+
@recent_jobs = paginate(sorted_query)
|
|
11
14
|
|
|
12
15
|
preload_job_statuses(@recent_jobs[:records])
|
|
13
16
|
|
|
@@ -31,7 +34,8 @@ module SolidQueueMonitor
|
|
|
31
34
|
SolidQueueMonitor::JobsPresenter.new(@recent_jobs[:records],
|
|
32
35
|
current_page: @recent_jobs[:current_page],
|
|
33
36
|
total_pages: @recent_jobs[:total_pages],
|
|
34
|
-
filters: filter_params
|
|
37
|
+
filters: filter_params,
|
|
38
|
+
sort: sort_params).render
|
|
35
39
|
end
|
|
36
40
|
end
|
|
37
41
|
end
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class QueuesController < BaseController
|
|
5
|
+
SORTABLE_COLUMNS = %w[queue_name job_count].freeze
|
|
6
|
+
QUEUE_DETAILS_SORTABLE_COLUMNS = %w[class_name created_at].freeze
|
|
7
|
+
|
|
5
8
|
def index
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
base_query = SolidQueue::Job.group(:queue_name)
|
|
10
|
+
.select('queue_name, COUNT(*) as job_count')
|
|
11
|
+
@queues = apply_queue_sorting(base_query)
|
|
9
12
|
@paused_queues = QueuePauseService.paused_queues
|
|
10
13
|
|
|
11
|
-
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues, @paused_queues).render)
|
|
14
|
+
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues, @paused_queues, sort: sort_params).render)
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def show
|
|
@@ -16,9 +19,9 @@ module SolidQueueMonitor
|
|
|
16
19
|
@paused = QueuePauseService.paused_queues.include?(@queue_name)
|
|
17
20
|
|
|
18
21
|
# Get all jobs for this queue with filtering and pagination
|
|
19
|
-
base_query = SolidQueue::Job.where(queue_name: @queue_name)
|
|
20
|
-
|
|
21
|
-
@jobs = paginate(
|
|
22
|
+
base_query = SolidQueue::Job.where(queue_name: @queue_name)
|
|
23
|
+
sorted_query = apply_sorting(filter_queue_jobs(base_query), QUEUE_DETAILS_SORTABLE_COLUMNS, 'created_at', :desc)
|
|
24
|
+
@jobs = paginate(sorted_query)
|
|
22
25
|
preload_job_statuses(@jobs[:records])
|
|
23
26
|
|
|
24
27
|
@counts = calculate_queue_counts(@queue_name)
|
|
@@ -31,7 +34,8 @@ module SolidQueueMonitor
|
|
|
31
34
|
counts: @counts,
|
|
32
35
|
current_page: @jobs[:current_page],
|
|
33
36
|
total_pages: @jobs[:total_pages],
|
|
34
|
-
filters: queue_filter_params
|
|
37
|
+
filters: queue_filter_params,
|
|
38
|
+
sort: sort_params
|
|
35
39
|
).render)
|
|
36
40
|
end
|
|
37
41
|
|
|
@@ -97,5 +101,14 @@ module SolidQueueMonitor
|
|
|
97
101
|
status: params[:status]
|
|
98
102
|
}
|
|
99
103
|
end
|
|
104
|
+
|
|
105
|
+
def apply_queue_sorting(relation)
|
|
106
|
+
column = sort_params[:sort_by]
|
|
107
|
+
direction = sort_params[:sort_direction]
|
|
108
|
+
column = 'job_count' unless SORTABLE_COLUMNS.include?(column)
|
|
109
|
+
direction = 'desc' unless %w[asc desc].include?(direction)
|
|
110
|
+
|
|
111
|
+
relation.order("#{column} #{direction}")
|
|
112
|
+
end
|
|
100
113
|
end
|
|
101
114
|
end
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class ReadyJobsController < BaseController
|
|
5
|
+
SORTABLE_COLUMNS = %w[class_name queue_name priority created_at].freeze
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
|
-
base_query = SolidQueue::ReadyExecution.includes(:job)
|
|
7
|
-
|
|
8
|
+
base_query = SolidQueue::ReadyExecution.includes(:job)
|
|
9
|
+
sorted_query = apply_execution_sorting(filter_ready_jobs(base_query), SORTABLE_COLUMNS, 'created_at', :desc)
|
|
10
|
+
@ready_jobs = paginate(sorted_query)
|
|
8
11
|
|
|
9
12
|
render_page('Ready Jobs', SolidQueueMonitor::ReadyJobsPresenter.new(@ready_jobs[:records],
|
|
10
13
|
current_page: @ready_jobs[:current_page],
|
|
11
14
|
total_pages: @ready_jobs[:total_pages],
|
|
12
|
-
filters: filter_params
|
|
15
|
+
filters: filter_params,
|
|
16
|
+
sort: sort_params).render)
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
end
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class RecurringJobsController < BaseController
|
|
5
|
+
SORTABLE_COLUMNS = %w[key class_name queue_name priority].freeze
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
|
-
base_query = filter_recurring_jobs(SolidQueue::RecurringTask.
|
|
7
|
-
|
|
8
|
+
base_query = filter_recurring_jobs(SolidQueue::RecurringTask.all)
|
|
9
|
+
sorted_query = apply_sorting(base_query, SORTABLE_COLUMNS, 'key', :asc)
|
|
10
|
+
@recurring_jobs = paginate(sorted_query)
|
|
8
11
|
|
|
9
12
|
render_page('Recurring Jobs', SolidQueueMonitor::RecurringJobsPresenter.new(@recurring_jobs[:records],
|
|
10
13
|
current_page: @recurring_jobs[:current_page],
|
|
11
14
|
total_pages: @recurring_jobs[:total_pages],
|
|
12
|
-
filters: filter_params
|
|
15
|
+
filters: filter_params,
|
|
16
|
+
sort: sort_params).render)
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
end
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class ScheduledJobsController < BaseController
|
|
5
|
+
SORTABLE_COLUMNS = %w[class_name queue_name scheduled_at].freeze
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
|
-
base_query = SolidQueue::ScheduledExecution.includes(:job)
|
|
7
|
-
|
|
8
|
+
base_query = SolidQueue::ScheduledExecution.includes(:job)
|
|
9
|
+
sorted_query = apply_execution_sorting(filter_scheduled_jobs(base_query), SORTABLE_COLUMNS, 'scheduled_at', :asc)
|
|
10
|
+
@scheduled_jobs = paginate(sorted_query)
|
|
8
11
|
|
|
9
12
|
render_page('Scheduled Jobs', SolidQueueMonitor::ScheduledJobsPresenter.new(@scheduled_jobs[:records],
|
|
10
13
|
current_page: @scheduled_jobs[:current_page],
|
|
11
14
|
total_pages: @scheduled_jobs[:total_pages],
|
|
12
|
-
filters: filter_params
|
|
15
|
+
filters: filter_params,
|
|
16
|
+
sort: sort_params).render)
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
def create
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueMonitor
|
|
4
|
+
class SearchController < BaseController
|
|
5
|
+
def index
|
|
6
|
+
query = params[:q]
|
|
7
|
+
results = SearchService.new(query).search
|
|
8
|
+
|
|
9
|
+
render_page('Search', SearchResultsPresenter.new(query, results).render, search_query: query)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class WorkersController < BaseController
|
|
5
|
+
SORTABLE_COLUMNS = %w[hostname last_heartbeat_at].freeze
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
|
-
base_query = SolidQueue::Process.
|
|
7
|
-
|
|
8
|
-
@processes = paginate(
|
|
8
|
+
base_query = SolidQueue::Process.all
|
|
9
|
+
sorted_query = apply_sorting(filter_workers(base_query), SORTABLE_COLUMNS, 'last_heartbeat_at', :desc)
|
|
10
|
+
@processes = paginate(sorted_query)
|
|
9
11
|
|
|
10
12
|
render_page('Workers', SolidQueueMonitor::WorkersPresenter.new(
|
|
11
13
|
@processes[:records],
|
|
12
14
|
current_page: @processes[:current_page],
|
|
13
15
|
total_pages: @processes[:total_pages],
|
|
14
|
-
filters: worker_filter_params
|
|
16
|
+
filters: worker_filter_params,
|
|
17
|
+
sort: sort_params
|
|
15
18
|
).render)
|
|
16
19
|
end
|
|
17
20
|
|
|
@@ -118,6 +118,34 @@ module SolidQueueMonitor
|
|
|
118
118
|
"<a href=\"#{queue_details_path(queue_name: queue_name)}\" class=\"#{classes}\">#{queue_name}</a>"
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
def sortable_header(column, label)
|
|
122
|
+
return "<th>#{label}</th>" unless @sort
|
|
123
|
+
|
|
124
|
+
column_str = column.to_s
|
|
125
|
+
is_active = @sort[:sort_by] == column_str
|
|
126
|
+
next_direction = is_active && @sort[:sort_direction] == 'asc' ? 'desc' : 'asc'
|
|
127
|
+
arrow = sort_arrow(is_active)
|
|
128
|
+
css_class = is_active ? 'sortable-header active' : 'sortable-header'
|
|
129
|
+
|
|
130
|
+
"<th><a href=\"?sort_by=#{column}&sort_direction=#{next_direction}#{filter_query_string}\" class=\"#{css_class}\">#{label}#{arrow}</a></th>"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def sort_arrow(is_active)
|
|
134
|
+
return ' ⇅' unless is_active
|
|
135
|
+
|
|
136
|
+
@sort[:sort_direction] == 'asc' ? ' ↑' : ' ↓'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def filter_query_string
|
|
140
|
+
params = []
|
|
141
|
+
params << "class_name=#{@filters[:class_name]}" if @filters && @filters[:class_name].present?
|
|
142
|
+
params << "queue_name=#{@filters[:queue_name]}" if @filters && @filters[:queue_name].present?
|
|
143
|
+
params << "arguments=#{@filters[:arguments]}" if @filters && @filters[:arguments].present?
|
|
144
|
+
params << "status=#{@filters[:status]}" if @filters && @filters[:status].present?
|
|
145
|
+
|
|
146
|
+
params.empty? ? '' : "&#{params.join('&')}"
|
|
147
|
+
end
|
|
148
|
+
|
|
121
149
|
def request_path
|
|
122
150
|
if defined?(controller) && controller.respond_to?(:request)
|
|
123
151
|
controller.request.path
|
|
@@ -138,14 +166,28 @@ module SolidQueueMonitor
|
|
|
138
166
|
private
|
|
139
167
|
|
|
140
168
|
def query_params
|
|
141
|
-
params =
|
|
142
|
-
params << "class_name=#{@filters[:class_name]}" if @filters && @filters[:class_name].present?
|
|
143
|
-
params << "queue_name=#{@filters[:queue_name]}" if @filters && @filters[:queue_name].present?
|
|
144
|
-
params << "status=#{@filters[:status]}" if @filters && @filters[:status].present?
|
|
145
|
-
|
|
169
|
+
params = build_filter_params + build_sort_params
|
|
146
170
|
params.empty? ? '' : "&#{params.join('&')}"
|
|
147
171
|
end
|
|
148
172
|
|
|
173
|
+
def build_filter_params
|
|
174
|
+
return [] unless @filters
|
|
175
|
+
|
|
176
|
+
filter_keys = %i[class_name queue_name status]
|
|
177
|
+
filter_keys.filter_map do |key|
|
|
178
|
+
"#{key}=#{@filters[key]}" if @filters[key].present?
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def build_sort_params
|
|
183
|
+
return [] unless @sort
|
|
184
|
+
|
|
185
|
+
sort_keys = %i[sort_by sort_direction]
|
|
186
|
+
sort_keys.filter_map do |key|
|
|
187
|
+
"#{key}=#{@sort[key]}" if @sort[key].present?
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
149
191
|
def full_path(route_name, *args)
|
|
150
192
|
SolidQueueMonitor::Engine.routes.url_helpers.send(route_name, *args)
|
|
151
193
|
rescue NoMethodError
|
|
@@ -5,11 +5,12 @@ module SolidQueueMonitor
|
|
|
5
5
|
include Rails.application.routes.url_helpers
|
|
6
6
|
include SolidQueueMonitor::Engine.routes.url_helpers
|
|
7
7
|
|
|
8
|
-
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
|
8
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
|
|
9
9
|
@jobs = jobs
|
|
10
10
|
@current_page = current_page
|
|
11
11
|
@total_pages = total_pages
|
|
12
12
|
@filters = filters
|
|
13
|
+
@sort = sort
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def render
|
|
@@ -60,10 +61,11 @@ module SolidQueueMonitor
|
|
|
60
61
|
<thead>
|
|
61
62
|
<tr>
|
|
62
63
|
<th><input type="checkbox" id="select-all" class="select-all-checkbox"></th>
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
#{sortable_header('class_name', 'Job')}
|
|
65
|
+
#{sortable_header('queue_name', 'Queue')}
|
|
65
66
|
<th>Error</th>
|
|
66
67
|
<th>Arguments</th>
|
|
68
|
+
#{sortable_header('created_at', 'Failed At')}
|
|
67
69
|
<th>Actions</th>
|
|
68
70
|
</tr>
|
|
69
71
|
</thead>
|
|
@@ -261,11 +263,9 @@ module SolidQueueMonitor
|
|
|
261
263
|
</td>
|
|
262
264
|
<td>
|
|
263
265
|
<div class="error-message">#{error[:message].to_s.truncate(100)}</div>
|
|
264
|
-
<div class="job-meta">
|
|
265
|
-
<span class="job-timestamp">Failed at: #{format_datetime(failed_execution.created_at)}</span>
|
|
266
|
-
</div>
|
|
267
266
|
</td>
|
|
268
267
|
<td>#{format_arguments(job.arguments)}</td>
|
|
268
|
+
<td>#{format_datetime(failed_execution.created_at)}</td>
|
|
269
269
|
<td class="actions-cell">
|
|
270
270
|
<div class="job-actions">
|
|
271
271
|
<a href="javascript:void(0)"#{' '}
|
|
@@ -4,11 +4,12 @@ module SolidQueueMonitor
|
|
|
4
4
|
class InProgressJobsPresenter < BasePresenter
|
|
5
5
|
include SolidQueueMonitor::Engine.routes.url_helpers
|
|
6
6
|
|
|
7
|
-
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
|
7
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
|
|
8
8
|
@jobs = jobs
|
|
9
9
|
@current_page = current_page
|
|
10
10
|
@total_pages = total_pages
|
|
11
11
|
@filters = filters
|
|
12
|
+
@sort = sort
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def render
|
|
@@ -47,10 +48,10 @@ module SolidQueueMonitor
|
|
|
47
48
|
<table>
|
|
48
49
|
<thead>
|
|
49
50
|
<tr>
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
#{sortable_header('class_name', 'Job')}
|
|
52
|
+
#{sortable_header('queue_name', 'Queue')}
|
|
52
53
|
<th>Arguments</th>
|
|
53
|
-
|
|
54
|
+
#{sortable_header('created_at', 'Started At')}
|
|
54
55
|
<th>Process ID</th>
|
|
55
56
|
</tr>
|
|
56
57
|
</thead>
|
|
@@ -5,11 +5,12 @@ module SolidQueueMonitor
|
|
|
5
5
|
include Rails.application.routes.url_helpers
|
|
6
6
|
include SolidQueueMonitor::Engine.routes.url_helpers
|
|
7
7
|
|
|
8
|
-
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
|
8
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
|
|
9
9
|
@jobs = jobs
|
|
10
10
|
@current_page = current_page
|
|
11
11
|
@total_pages = total_pages
|
|
12
12
|
@filters = filters
|
|
13
|
+
@sort = sort
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def render
|
|
@@ -73,11 +74,11 @@ module SolidQueueMonitor
|
|
|
73
74
|
<thead>
|
|
74
75
|
<tr>
|
|
75
76
|
<th>ID</th>
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
#{sortable_header('class_name', 'Job')}
|
|
78
|
+
#{sortable_header('queue_name', 'Queue')}
|
|
78
79
|
<th>Arguments</th>
|
|
79
80
|
<th>Status</th>
|
|
80
|
-
|
|
81
|
+
#{sortable_header('created_at', 'Created At')}
|
|
81
82
|
<th>Actions</th>
|
|
82
83
|
</tr>
|
|
83
84
|
</thead>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class QueueDetailsPresenter < BasePresenter
|
|
5
|
-
def initialize(queue_name:, paused:, jobs:, counts:, current_page: 1, total_pages: 1, filters: {})
|
|
5
|
+
def initialize(queue_name:, paused:, jobs:, counts:, current_page: 1, total_pages: 1, filters: {}, sort: {})
|
|
6
6
|
@queue_name = queue_name
|
|
7
7
|
@paused = paused
|
|
8
8
|
@jobs = jobs
|
|
@@ -10,6 +10,7 @@ module SolidQueueMonitor
|
|
|
10
10
|
@current_page = current_page
|
|
11
11
|
@total_pages = total_pages
|
|
12
12
|
@filters = filters
|
|
13
|
+
@sort = sort
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def render
|
|
@@ -129,10 +130,10 @@ module SolidQueueMonitor
|
|
|
129
130
|
<thead>
|
|
130
131
|
<tr>
|
|
131
132
|
<th>ID</th>
|
|
132
|
-
|
|
133
|
+
#{sortable_header('class_name', 'Job')}
|
|
133
134
|
<th>Arguments</th>
|
|
134
135
|
<th>Status</th>
|
|
135
|
-
|
|
136
|
+
#{sortable_header('created_at', 'Created At')}
|
|
136
137
|
<th>Actions</th>
|
|
137
138
|
</tr>
|
|
138
139
|
</thead>
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class QueuesPresenter < BasePresenter
|
|
5
|
-
def initialize(records, paused_queues = [])
|
|
5
|
+
def initialize(records, paused_queues = [], sort: {})
|
|
6
6
|
@records = records
|
|
7
7
|
@paused_queues = paused_queues
|
|
8
|
+
@sort = sort
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def render
|
|
@@ -19,9 +20,9 @@ module SolidQueueMonitor
|
|
|
19
20
|
<table>
|
|
20
21
|
<thead>
|
|
21
22
|
<tr>
|
|
22
|
-
|
|
23
|
+
#{sortable_header('queue_name', 'Queue Name')}
|
|
23
24
|
<th>Status</th>
|
|
24
|
-
|
|
25
|
+
#{sortable_header('job_count', 'Total Jobs')}
|
|
25
26
|
<th>Ready Jobs</th>
|
|
26
27
|
<th>Scheduled Jobs</th>
|
|
27
28
|
<th>Failed Jobs</th>
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class ReadyJobsPresenter < BasePresenter
|
|
5
|
-
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
|
5
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
|
|
6
6
|
@jobs = jobs
|
|
7
7
|
@current_page = current_page
|
|
8
8
|
@total_pages = total_pages
|
|
9
9
|
@filters = filters
|
|
10
|
+
@sort = sort
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def render
|
|
@@ -50,11 +51,11 @@ module SolidQueueMonitor
|
|
|
50
51
|
<table>
|
|
51
52
|
<thead>
|
|
52
53
|
<tr>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
#{sortable_header('class_name', 'Job')}
|
|
55
|
+
#{sortable_header('queue_name', 'Queue')}
|
|
56
|
+
#{sortable_header('priority', 'Priority')}
|
|
56
57
|
<th>Arguments</th>
|
|
57
|
-
|
|
58
|
+
#{sortable_header('created_at', 'Created At')}
|
|
58
59
|
</tr>
|
|
59
60
|
</thead>
|
|
60
61
|
<tbody>
|
|
@@ -5,11 +5,12 @@ module SolidQueueMonitor
|
|
|
5
5
|
include Rails.application.routes.url_helpers
|
|
6
6
|
include SolidQueueMonitor::Engine.routes.url_helpers
|
|
7
7
|
|
|
8
|
-
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
|
8
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
|
|
9
9
|
@jobs = jobs
|
|
10
10
|
@current_page = current_page
|
|
11
11
|
@total_pages = total_pages
|
|
12
12
|
@filters = filters
|
|
13
|
+
@sort = sort
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def render
|
|
@@ -48,11 +49,11 @@ module SolidQueueMonitor
|
|
|
48
49
|
<table>
|
|
49
50
|
<thead>
|
|
50
51
|
<tr>
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
#{sortable_header('key', 'Key')}
|
|
53
|
+
#{sortable_header('class_name', 'Job')}
|
|
53
54
|
<th>Schedule</th>
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
#{sortable_header('queue_name', 'Queue')}
|
|
56
|
+
#{sortable_header('priority', 'Priority')}
|
|
56
57
|
<th>Last Updated</th>
|
|
57
58
|
</tr>
|
|
58
59
|
</thead>
|
|
@@ -5,11 +5,12 @@ module SolidQueueMonitor
|
|
|
5
5
|
include Rails.application.routes.url_helpers
|
|
6
6
|
include SolidQueueMonitor::Engine.routes.url_helpers
|
|
7
7
|
|
|
8
|
-
def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
|
|
8
|
+
def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
|
|
9
9
|
@jobs = jobs
|
|
10
10
|
@current_page = current_page
|
|
11
11
|
@total_pages = total_pages
|
|
12
12
|
@filters = filters
|
|
13
|
+
@sort = sort
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def render
|
|
@@ -140,9 +141,9 @@ module SolidQueueMonitor
|
|
|
140
141
|
<thead>
|
|
141
142
|
<tr>
|
|
142
143
|
<th width="50"><input type="checkbox"></th>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
#{sortable_header('class_name', 'Job')}
|
|
145
|
+
#{sortable_header('queue_name', 'Queue')}
|
|
146
|
+
#{sortable_header('scheduled_at', 'Scheduled At')}
|
|
146
147
|
<th>Arguments</th>
|
|
147
148
|
</tr>
|
|
148
149
|
</thead>
|
|
@@ -0,0 +1,190 @@
|
|
|
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('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -5,11 +5,12 @@ module SolidQueueMonitor
|
|
|
5
5
|
HEARTBEAT_STALE_THRESHOLD = 5.minutes
|
|
6
6
|
HEARTBEAT_DEAD_THRESHOLD = 10.minutes
|
|
7
7
|
|
|
8
|
-
def initialize(processes, current_page: 1, total_pages: 1, filters: {})
|
|
8
|
+
def initialize(processes, current_page: 1, total_pages: 1, filters: {}, sort: {})
|
|
9
9
|
@processes = processes.to_a # Load records once to avoid multiple queries
|
|
10
10
|
@current_page = current_page
|
|
11
11
|
@total_pages = total_pages
|
|
12
12
|
@filters = filters
|
|
13
|
+
@sort = sort
|
|
13
14
|
preload_claimed_data
|
|
14
15
|
calculate_summary_stats
|
|
15
16
|
end
|
|
@@ -140,10 +141,10 @@ module SolidQueueMonitor
|
|
|
140
141
|
<thead>
|
|
141
142
|
<tr>
|
|
142
143
|
<th>Kind</th>
|
|
143
|
-
|
|
144
|
+
#{sortable_header('hostname', 'Hostname')}
|
|
144
145
|
<th>PID</th>
|
|
145
146
|
<th>Queues</th>
|
|
146
|
-
|
|
147
|
+
#{sortable_header('last_heartbeat_at', 'Last Heartbeat')}
|
|
147
148
|
<th>Status</th>
|
|
148
149
|
<th>Jobs Processing</th>
|
|
149
150
|
<th>Actions</th>
|
|
@@ -5,11 +5,12 @@ module SolidQueueMonitor
|
|
|
5
5
|
include Rails.application.routes.url_helpers
|
|
6
6
|
include SolidQueueMonitor::Engine.routes.url_helpers
|
|
7
7
|
|
|
8
|
-
def initialize(title:, content:, message: nil, message_type: nil)
|
|
8
|
+
def initialize(title:, content:, message: nil, message_type: nil, search_query: nil)
|
|
9
9
|
@title = title
|
|
10
10
|
@content = content
|
|
11
11
|
@message = message
|
|
12
12
|
@message_type = message_type
|
|
13
|
+
@search_query = search_query
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def generate
|
|
@@ -107,7 +108,8 @@ module SolidQueueMonitor
|
|
|
107
108
|
<<-HTML
|
|
108
109
|
<header>
|
|
109
110
|
<div class="header-top">
|
|
110
|
-
<h1>Solid Queue Monitor</h1>
|
|
111
|
+
<h1><a href="#{root_path}" class="header-title-link">Solid Queue Monitor</a></h1>
|
|
112
|
+
#{generate_search_box}
|
|
111
113
|
<div class="header-controls">
|
|
112
114
|
#{generate_auto_refresh_controls}
|
|
113
115
|
#{generate_theme_toggle}
|
|
@@ -128,6 +130,25 @@ module SolidQueueMonitor
|
|
|
128
130
|
HTML
|
|
129
131
|
end
|
|
130
132
|
|
|
133
|
+
def generate_search_box
|
|
134
|
+
search_value = @search_query ? escape_html(@search_query) : ''
|
|
135
|
+
<<-HTML
|
|
136
|
+
<form method="get" action="#{search_path}" class="header-search-form">
|
|
137
|
+
<input type="text" name="q" value="#{search_value}" placeholder="Search by class, queue, job ID, or error..." class="header-search-input">
|
|
138
|
+
<button type="submit" class="header-search-button" title="Search">
|
|
139
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
140
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
141
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
142
|
+
</svg>
|
|
143
|
+
</button>
|
|
144
|
+
</form>
|
|
145
|
+
HTML
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def escape_html(text)
|
|
149
|
+
text.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
150
|
+
end
|
|
151
|
+
|
|
131
152
|
def generate_auto_refresh_controls
|
|
132
153
|
return '' unless SolidQueueMonitor.auto_refresh_enabled
|
|
133
154
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueMonitor
|
|
4
|
+
class SearchService
|
|
5
|
+
RESULTS_LIMIT = 25
|
|
6
|
+
|
|
7
|
+
def initialize(query)
|
|
8
|
+
@query = query
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def search
|
|
12
|
+
return empty_results if @query.blank?
|
|
13
|
+
|
|
14
|
+
term = "%#{sanitize_query(@query)}%"
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
ready: search_ready_jobs(term),
|
|
18
|
+
scheduled: search_scheduled_jobs(term),
|
|
19
|
+
failed: search_failed_jobs(term),
|
|
20
|
+
in_progress: search_in_progress_jobs(term),
|
|
21
|
+
completed: search_completed_jobs(term),
|
|
22
|
+
recurring: search_recurring_tasks(term)
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def empty_results
|
|
29
|
+
{
|
|
30
|
+
ready: [],
|
|
31
|
+
scheduled: [],
|
|
32
|
+
failed: [],
|
|
33
|
+
in_progress: [],
|
|
34
|
+
completed: [],
|
|
35
|
+
recurring: []
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def sanitize_query(query)
|
|
40
|
+
# Escape % to prevent LIKE pattern injection
|
|
41
|
+
# We don't escape _ because it requires database-specific ESCAPE clauses
|
|
42
|
+
query.to_s.gsub('%', '\%')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def search_ready_jobs(term)
|
|
46
|
+
SolidQueue::ReadyExecution
|
|
47
|
+
.joins(:job)
|
|
48
|
+
.where(job_search_conditions, term: term)
|
|
49
|
+
.includes(:job)
|
|
50
|
+
.limit(RESULTS_LIMIT)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def search_scheduled_jobs(term)
|
|
54
|
+
SolidQueue::ScheduledExecution
|
|
55
|
+
.joins(:job)
|
|
56
|
+
.where(job_search_conditions, term: term)
|
|
57
|
+
.includes(:job)
|
|
58
|
+
.limit(RESULTS_LIMIT)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def search_failed_jobs(term)
|
|
62
|
+
SolidQueue::FailedExecution
|
|
63
|
+
.joins(:job)
|
|
64
|
+
.where(failed_job_search_conditions, term: term)
|
|
65
|
+
.includes(:job)
|
|
66
|
+
.limit(RESULTS_LIMIT)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def search_in_progress_jobs(term)
|
|
70
|
+
SolidQueue::ClaimedExecution
|
|
71
|
+
.joins(:job)
|
|
72
|
+
.where(job_search_conditions, term: term)
|
|
73
|
+
.includes(:job)
|
|
74
|
+
.limit(RESULTS_LIMIT)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def search_completed_jobs(term)
|
|
78
|
+
SolidQueue::Job
|
|
79
|
+
.where.not(finished_at: nil)
|
|
80
|
+
.where(completed_job_search_conditions, term: term)
|
|
81
|
+
.order(finished_at: :desc)
|
|
82
|
+
.limit(RESULTS_LIMIT)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def search_recurring_tasks(term)
|
|
86
|
+
SolidQueue::RecurringTask
|
|
87
|
+
.where(recurring_task_search_conditions, term: term)
|
|
88
|
+
.limit(RESULTS_LIMIT)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def job_search_conditions
|
|
92
|
+
<<~SQL.squish
|
|
93
|
+
solid_queue_jobs.class_name LIKE :term
|
|
94
|
+
OR solid_queue_jobs.queue_name LIKE :term
|
|
95
|
+
OR solid_queue_jobs.arguments LIKE :term
|
|
96
|
+
OR solid_queue_jobs.active_job_id LIKE :term
|
|
97
|
+
SQL
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def failed_job_search_conditions
|
|
101
|
+
<<~SQL.squish
|
|
102
|
+
solid_queue_jobs.class_name LIKE :term
|
|
103
|
+
OR solid_queue_jobs.queue_name LIKE :term
|
|
104
|
+
OR solid_queue_jobs.arguments LIKE :term
|
|
105
|
+
OR solid_queue_jobs.active_job_id LIKE :term
|
|
106
|
+
OR solid_queue_failed_executions.error LIKE :term
|
|
107
|
+
SQL
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def completed_job_search_conditions
|
|
111
|
+
<<~SQL.squish
|
|
112
|
+
class_name LIKE :term
|
|
113
|
+
OR queue_name LIKE :term
|
|
114
|
+
OR arguments LIKE :term
|
|
115
|
+
OR active_job_id LIKE :term
|
|
116
|
+
SQL
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def recurring_task_search_conditions
|
|
120
|
+
<<~SQL.squish
|
|
121
|
+
solid_queue_recurring_tasks.key LIKE :term
|
|
122
|
+
OR solid_queue_recurring_tasks.class_name LIKE :term
|
|
123
|
+
SQL
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -66,6 +66,16 @@ module SolidQueueMonitor
|
|
|
66
66
|
margin-bottom: 0.5rem;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
.solid_queue_monitor .header-title-link {
|
|
70
|
+
color: var(--text-color);
|
|
71
|
+
text-decoration: none;
|
|
72
|
+
transition: color 0.2s;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.solid_queue_monitor .header-title-link:hover {
|
|
76
|
+
color: var(--primary-color);
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
.solid_queue_monitor .navigation {
|
|
70
80
|
display: flex;
|
|
71
81
|
flex-wrap: wrap;
|
|
@@ -195,6 +205,22 @@ module SolidQueueMonitor
|
|
|
195
205
|
color: var(--text-muted);
|
|
196
206
|
}
|
|
197
207
|
|
|
208
|
+
.solid_queue_monitor .sortable-header {
|
|
209
|
+
color: var(--text-muted);
|
|
210
|
+
text-decoration: none;
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
transition: color 0.2s;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.solid_queue_monitor .sortable-header:hover {
|
|
216
|
+
color: var(--primary-color);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.solid_queue_monitor .sortable-header.active {
|
|
220
|
+
color: var(--primary-color);
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
}
|
|
223
|
+
|
|
198
224
|
.solid_queue_monitor .status-badge {
|
|
199
225
|
display: inline-block;
|
|
200
226
|
padding: 0.25rem 0.5rem;
|
|
@@ -1178,6 +1204,98 @@ module SolidQueueMonitor
|
|
|
1178
1204
|
gap: 0.75rem;
|
|
1179
1205
|
}
|
|
1180
1206
|
|
|
1207
|
+
/* Header Search Box */
|
|
1208
|
+
.solid_queue_monitor .header-search-form {
|
|
1209
|
+
display: flex;
|
|
1210
|
+
align-items: center;
|
|
1211
|
+
gap: 0;
|
|
1212
|
+
flex: 1;
|
|
1213
|
+
max-width: 400px;
|
|
1214
|
+
margin: 0 1rem;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.solid_queue_monitor .header-search-input {
|
|
1218
|
+
flex: 1;
|
|
1219
|
+
padding: 0.5rem 0.75rem;
|
|
1220
|
+
border: 1px solid var(--input-border);
|
|
1221
|
+
border-right: none;
|
|
1222
|
+
border-radius: 0.375rem 0 0 0.375rem;
|
|
1223
|
+
font-size: 0.875rem;
|
|
1224
|
+
background: var(--input-background);
|
|
1225
|
+
color: var(--text-color);
|
|
1226
|
+
outline: none;
|
|
1227
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.solid_queue_monitor .header-search-input:focus {
|
|
1231
|
+
border-color: var(--primary-color);
|
|
1232
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.solid_queue_monitor .header-search-input::placeholder {
|
|
1236
|
+
color: var(--text-muted);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.solid_queue_monitor .header-search-button {
|
|
1240
|
+
display: flex;
|
|
1241
|
+
align-items: center;
|
|
1242
|
+
justify-content: center;
|
|
1243
|
+
padding: 0.5rem 0.75rem;
|
|
1244
|
+
background: var(--primary-color);
|
|
1245
|
+
color: white;
|
|
1246
|
+
border: 1px solid var(--primary-color);
|
|
1247
|
+
border-radius: 0 0.375rem 0.375rem 0;
|
|
1248
|
+
cursor: pointer;
|
|
1249
|
+
transition: background-color 0.2s;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.solid_queue_monitor .header-search-button:hover {
|
|
1253
|
+
background: #2563eb;
|
|
1254
|
+
border-color: #2563eb;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.solid_queue_monitor .header-search-button svg {
|
|
1258
|
+
width: 16px;
|
|
1259
|
+
height: 16px;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
@media (max-width: 768px) {
|
|
1263
|
+
.solid_queue_monitor .header-search-form {
|
|
1264
|
+
max-width: 100%;
|
|
1265
|
+
margin: 0.5rem 0;
|
|
1266
|
+
order: 3;
|
|
1267
|
+
width: 100%;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/* Search Results Page */
|
|
1272
|
+
.solid_queue_monitor .results-summary {
|
|
1273
|
+
margin: 1rem 0;
|
|
1274
|
+
padding: 0.75rem 1rem;
|
|
1275
|
+
background: var(--card-background);
|
|
1276
|
+
border-radius: 0.375rem;
|
|
1277
|
+
box-shadow: var(--card-shadow);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
.solid_queue_monitor .results-summary p {
|
|
1281
|
+
margin: 0;
|
|
1282
|
+
color: var(--text-muted);
|
|
1283
|
+
font-size: 0.875rem;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
.solid_queue_monitor .search-results-section {
|
|
1287
|
+
margin-top: 1.5rem;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
.solid_queue_monitor .search-results-section h3 {
|
|
1291
|
+
font-size: 1rem;
|
|
1292
|
+
font-weight: 600;
|
|
1293
|
+
color: var(--text-color);
|
|
1294
|
+
margin-bottom: 0.75rem;
|
|
1295
|
+
padding-bottom: 0.5rem;
|
|
1296
|
+
border-bottom: 1px solid var(--border-color);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1181
1299
|
/* Workers Page Styles */
|
|
1182
1300
|
.solid_queue_monitor .workers-summary {
|
|
1183
1301
|
display: grid;
|
data/config/routes.rb
CHANGED
|
@@ -7,6 +7,7 @@ SolidQueueMonitor::Engine.routes.draw do
|
|
|
7
7
|
root to: 'overview#index'
|
|
8
8
|
|
|
9
9
|
get 'chart_data', to: 'overview#chart_data', as: :chart_data
|
|
10
|
+
get 'search', to: 'search#index', as: :search
|
|
10
11
|
|
|
11
12
|
resources :ready_jobs, only: [:index]
|
|
12
13
|
resources :scheduled_jobs, only: [:index]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_queue_monitor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vishal Sadriya
|
|
@@ -58,6 +58,7 @@ files:
|
|
|
58
58
|
- app/controllers/solid_queue_monitor/ready_jobs_controller.rb
|
|
59
59
|
- app/controllers/solid_queue_monitor/recurring_jobs_controller.rb
|
|
60
60
|
- app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb
|
|
61
|
+
- app/controllers/solid_queue_monitor/search_controller.rb
|
|
61
62
|
- app/controllers/solid_queue_monitor/workers_controller.rb
|
|
62
63
|
- app/presenters/solid_queue_monitor/base_presenter.rb
|
|
63
64
|
- app/presenters/solid_queue_monitor/failed_jobs_presenter.rb
|
|
@@ -69,6 +70,7 @@ files:
|
|
|
69
70
|
- app/presenters/solid_queue_monitor/ready_jobs_presenter.rb
|
|
70
71
|
- app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb
|
|
71
72
|
- app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb
|
|
73
|
+
- app/presenters/solid_queue_monitor/search_results_presenter.rb
|
|
72
74
|
- app/presenters/solid_queue_monitor/stats_presenter.rb
|
|
73
75
|
- app/presenters/solid_queue_monitor/workers_presenter.rb
|
|
74
76
|
- app/services/solid_queue_monitor/authentication_service.rb
|
|
@@ -80,6 +82,7 @@ files:
|
|
|
80
82
|
- app/services/solid_queue_monitor/pagination_service.rb
|
|
81
83
|
- app/services/solid_queue_monitor/queue_pause_service.rb
|
|
82
84
|
- app/services/solid_queue_monitor/reject_job_service.rb
|
|
85
|
+
- app/services/solid_queue_monitor/search_service.rb
|
|
83
86
|
- app/services/solid_queue_monitor/stats_calculator.rb
|
|
84
87
|
- app/services/solid_queue_monitor/status_calculator.rb
|
|
85
88
|
- app/services/solid_queue_monitor/stylesheet_generator.rb
|
|
@@ -116,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
116
119
|
- !ruby/object:Gem::Version
|
|
117
120
|
version: '0'
|
|
118
121
|
requirements: []
|
|
119
|
-
rubygems_version:
|
|
122
|
+
rubygems_version: 4.0.6
|
|
120
123
|
specification_version: 4
|
|
121
124
|
summary: Simple monitoring interface for Solid Queue
|
|
122
125
|
test_files: []
|