solid_queue_monitor 1.0.1 → 1.2.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -1
  3. data/app/controllers/solid_queue_monitor/base_controller.rb +49 -33
  4. data/app/controllers/solid_queue_monitor/failed_jobs_controller.rb +7 -3
  5. data/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb +9 -7
  6. data/app/controllers/solid_queue_monitor/overview_controller.rb +13 -9
  7. data/app/controllers/solid_queue_monitor/queues_controller.rb +39 -16
  8. data/app/controllers/solid_queue_monitor/ready_jobs_controller.rb +7 -3
  9. data/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb +7 -3
  10. data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +7 -3
  11. data/app/controllers/solid_queue_monitor/search_controller.rb +12 -0
  12. data/app/controllers/solid_queue_monitor/workers_controller.rb +7 -4
  13. data/app/presenters/solid_queue_monitor/base_presenter.rb +47 -5
  14. data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +6 -6
  15. data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +5 -4
  16. data/app/presenters/solid_queue_monitor/jobs_presenter.rb +5 -4
  17. data/app/presenters/solid_queue_monitor/queue_details_presenter.rb +4 -3
  18. data/app/presenters/solid_queue_monitor/queues_presenter.rb +10 -22
  19. data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +6 -5
  20. data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +6 -5
  21. data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +5 -4
  22. data/app/presenters/solid_queue_monitor/search_results_presenter.rb +190 -0
  23. data/app/presenters/solid_queue_monitor/stats_presenter.rb +1 -2
  24. data/app/presenters/solid_queue_monitor/workers_presenter.rb +4 -3
  25. data/app/services/solid_queue_monitor/chart_data_service.rb +53 -57
  26. data/app/services/solid_queue_monitor/html_generator.rb +23 -2
  27. data/app/services/solid_queue_monitor/search_service.rb +126 -0
  28. data/app/services/solid_queue_monitor/stats_calculator.rb +12 -8
  29. data/app/services/solid_queue_monitor/stylesheet_generator.rb +118 -0
  30. data/config/routes.rb +1 -0
  31. data/lib/generators/solid_queue_monitor/templates/initializer.rb +3 -0
  32. data/lib/solid_queue_monitor/version.rb +1 -1
  33. data/lib/solid_queue_monitor.rb +2 -1
  34. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c553f4dcd107a4dbeeb008daa21593d2257ad6885d22da7dd98531fe2e91c0d
4
- data.tar.gz: 641f2c00f74fb0bbcef85d22f3ad9d8cae809b3624bfb48f8366a03ed7ab924d
3
+ metadata.gz: bc739f10c21ca234c61c65a5b94d9c548cfc8d332dac0d1db3311d0d113fe035
4
+ data.tar.gz: 4a131b01c6c3e330b46413029278dbcee09e4f6410f1edf44b14da5902ec5b1a
5
5
  SHA512:
6
- metadata.gz: fa09c1f8f00e1add829d6c77dc9acc81833ce247c504b4c0dd3f916b2fd399e6684c6b5dc9ceb1942ea19a42219ef2b2311e42fe069ece66cf897dc3062fa0d5
7
- data.tar.gz: '079cffeabf96b8f22fef6a41acf074aba277ee6e65faf8a148f80b66ea8bad935cc80363f355b48dcd41200d2b360791a64dc7c2d0377882bce02ed12352e1f4'
6
+ metadata.gz: 02b79913620ac69eb63c15b2c92a1ba76f44f981036f977eb72fe684faa28ae2057ea187582e8bbd5cb09c0de9f030be89e07b8e8e8e754ee0ffebd6e7c33f84
7
+ data.tar.gz: 853dc16edd433afa568a7cef1efb78b295d54595a72e1e7c177e7991ff50b3e396d80ab1092754e65972dbe0fa9072e2a4563ab68b4785ebe7b5d77b5373e086
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.0.1'
75
+ gem 'solid_queue_monitor', '~> 1.2'
74
76
  ```
75
77
 
76
78
  Then execute:
@@ -116,9 +118,22 @@ SolidQueueMonitor.setup do |config|
116
118
 
117
119
  # Auto-refresh interval in seconds (default: 30)
118
120
  config.auto_refresh_interval = 30
121
+
122
+ # Disable the chart on the overview page to skip chart queries entirely
123
+ # config.show_chart = true
119
124
  end
120
125
  ```
121
126
 
127
+ ### Performance at Scale
128
+
129
+ SolidQueueMonitor is optimized for large datasets (millions of rows in `solid_queue_jobs`). All dashboard queries are designed to stay fast regardless of table size.
130
+
131
+ If you don't need the job activity chart, disable it to skip chart queries entirely:
132
+
133
+ ```ruby
134
+ config.show_chart = false
135
+ ```
136
+
122
137
  ### Authentication
123
138
 
124
139
  By default, Solid Queue Monitor does not require authentication to access the dashboard. This makes it easy to get started in development environments.
@@ -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
@@ -90,17 +91,13 @@ module SolidQueueMonitor
90
91
  when 'completed'
91
92
  relation = relation.where.not(finished_at: nil)
92
93
  when 'failed'
93
- failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
94
- relation = relation.where(id: failed_job_ids)
94
+ relation = relation.where(id: SolidQueue::FailedExecution.select(:job_id))
95
95
  when 'scheduled'
96
- scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
97
- relation = relation.where(id: scheduled_job_ids)
96
+ relation = relation.where(id: SolidQueue::ScheduledExecution.select(:job_id))
98
97
  when 'pending'
99
- # Pending jobs are those that are not completed, failed, or scheduled
100
- failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
101
- scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
102
98
  relation = relation.where(finished_at: nil)
103
- .where.not(id: failed_job_ids + scheduled_job_ids)
99
+ .where.not(id: SolidQueue::FailedExecution.select(:job_id))
100
+ .where.not(id: SolidQueue::ScheduledExecution.select(:job_id))
104
101
  end
105
102
  end
106
103
 
@@ -116,16 +113,13 @@ module SolidQueueMonitor
116
113
  return relation unless params[:class_name].present? || params[:queue_name].present? || params[:arguments].present?
117
114
 
118
115
  if params[:class_name].present?
119
- job_ids = SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").pluck(:id)
120
- relation = relation.where(job_id: job_ids)
116
+ relation = relation.where(job_id: SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").select(:id))
121
117
  end
122
118
 
123
119
  relation = relation.where('queue_name LIKE ?', "%#{params[:queue_name]}%") if params[:queue_name].present?
124
120
 
125
- # Add arguments filtering
126
121
  if params[:arguments].present?
127
- job_ids = SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").pluck(:id)
128
- relation = relation.where(job_id: job_ids)
122
+ relation = relation.where(job_id: SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").select(:id))
129
123
  end
130
124
 
131
125
  relation
@@ -135,16 +129,13 @@ module SolidQueueMonitor
135
129
  return relation unless params[:class_name].present? || params[:queue_name].present? || params[:arguments].present?
136
130
 
137
131
  if params[:class_name].present?
138
- job_ids = SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").pluck(:id)
139
- relation = relation.where(job_id: job_ids)
132
+ relation = relation.where(job_id: SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").select(:id))
140
133
  end
141
134
 
142
135
  relation = relation.where('queue_name LIKE ?', "%#{params[:queue_name]}%") if params[:queue_name].present?
143
136
 
144
- # Add arguments filtering
145
137
  if params[:arguments].present?
146
- job_ids = SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").pluck(:id)
147
- relation = relation.where(job_id: job_ids)
138
+ relation = relation.where(job_id: SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").select(:id))
148
139
  end
149
140
 
150
141
  relation
@@ -169,25 +160,19 @@ module SolidQueueMonitor
169
160
  return relation unless params[:class_name].present? || params[:queue_name].present? || params[:arguments].present?
170
161
 
171
162
  if params[:class_name].present?
172
- job_ids = SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").pluck(:id)
173
- relation = relation.where(job_id: job_ids)
163
+ relation = relation.where(job_id: SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").select(:id))
174
164
  end
175
165
 
176
166
  if params[:queue_name].present?
177
- # Check if FailedExecution has queue_name column
178
- if relation.column_names.include?('queue_name')
179
- relation = relation.where('queue_name LIKE ?', "%#{params[:queue_name]}%")
180
- else
181
- # If not, filter by job's queue_name
182
- job_ids = SolidQueue::Job.where('queue_name LIKE ?', "%#{params[:queue_name]}%").pluck(:id)
183
- relation = relation.where(job_id: job_ids)
184
- end
167
+ relation = if relation.column_names.include?('queue_name')
168
+ relation.where('queue_name LIKE ?', "%#{params[:queue_name]}%")
169
+ else
170
+ relation.where(job_id: SolidQueue::Job.where('queue_name LIKE ?', "%#{params[:queue_name]}%").select(:id))
171
+ end
185
172
  end
186
173
 
187
- # Add arguments filtering
188
174
  if params[:arguments].present?
189
- job_ids = SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").pluck(:id)
190
- relation = relation.where(job_id: job_ids)
175
+ relation = relation.where(job_id: SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").select(:id))
191
176
  end
192
177
 
193
178
  relation
@@ -201,5 +186,36 @@ module SolidQueueMonitor
201
186
  status: params[:status]
202
187
  }
203
188
  end
189
+
190
+ def sort_params
191
+ {
192
+ sort_by: params[:sort_by],
193
+ sort_direction: params[:sort_direction]
194
+ }
195
+ end
196
+
197
+ def apply_sorting(relation, allowed_columns, default_column, default_direction = :desc)
198
+ column = sort_params[:sort_by]
199
+ direction = sort_params[:sort_direction]
200
+ column = default_column unless allowed_columns.include?(column)
201
+ direction = %w[asc desc].include?(direction) ? direction.to_sym : default_direction
202
+ relation.order(column => direction)
203
+ end
204
+
205
+ def apply_execution_sorting(relation, allowed_columns, default_column, default_direction = :desc)
206
+ column = sort_params[:sort_by]
207
+ direction = sort_params[:sort_direction]
208
+ column = default_column unless allowed_columns.include?(column)
209
+ direction = %w[asc desc].include?(direction) ? direction.to_sym : default_direction
210
+
211
+ # Columns that exist on the jobs table, not on execution tables
212
+ job_table_columns = %w[class_name queue_name]
213
+
214
+ if job_table_columns.include?(column)
215
+ relation.joins(:job).order("solid_queue_jobs.#{column}" => direction)
216
+ else
217
+ relation.order(column => direction)
218
+ end
219
+ end
204
220
  end
205
221
  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).order(created_at: :desc)
7
- @failed_jobs = paginate(filter_failed_jobs(base_query))
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).render)
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).order(created_at: :desc)
7
- @in_progress_jobs = paginate(filter_in_progress_jobs(base_query))
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).render)
15
+ filters: filter_params,
16
+ sort: sort_params).render)
13
17
  end
14
18
 
15
19
  private
@@ -18,13 +22,11 @@ module SolidQueueMonitor
18
22
  return relation if params[:class_name].blank? && params[:arguments].blank?
19
23
 
20
24
  if params[:class_name].present?
21
- job_ids = SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").pluck(:id)
22
- relation = relation.where(job_id: job_ids)
25
+ relation = relation.where(job_id: SolidQueue::Job.where('class_name LIKE ?', "%#{params[:class_name]}%").select(:id))
23
26
  end
24
27
 
25
28
  if params[:arguments].present?
26
- job_ids = SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").pluck(:id)
27
- relation = relation.where(job_id: job_ids)
29
+ relation = relation.where(job_id: SolidQueue::Job.where('arguments::text ILIKE ?', "%#{params[:arguments]}%").select(:id))
28
30
  end
29
31
 
30
32
  relation
@@ -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
- @chart_data = SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate
9
+ @chart_data = SolidQueueMonitor.show_chart ? SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate : nil
8
10
 
9
- recent_jobs_query = SolidQueue::Job.order(created_at: :desc).limit(100)
10
- @recent_jobs = paginate(filter_jobs(recent_jobs_query))
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
 
@@ -26,12 +29,13 @@ module SolidQueueMonitor
26
29
  end
27
30
 
28
31
  def generate_overview_content
29
- SolidQueueMonitor::StatsPresenter.new(@stats).render +
30
- SolidQueueMonitor::ChartPresenter.new(@chart_data).render +
31
- SolidQueueMonitor::JobsPresenter.new(@recent_jobs[:records],
32
- current_page: @recent_jobs[:current_page],
33
- total_pages: @recent_jobs[:total_pages],
34
- filters: filter_params).render
32
+ html = SolidQueueMonitor::StatsPresenter.new(@stats).render
33
+ html += SolidQueueMonitor::ChartPresenter.new(@chart_data).render if @chart_data
34
+ html + SolidQueueMonitor::JobsPresenter.new(@recent_jobs[:records],
35
+ current_page: @recent_jobs[:current_page],
36
+ total_pages: @recent_jobs[:total_pages],
37
+ filters: filter_params,
38
+ sort: sort_params).render
35
39
  end
36
40
  end
37
41
  end
@@ -2,13 +2,21 @@
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
- @queues = SolidQueue::Job.group(:queue_name)
7
- .select('queue_name, COUNT(*) as job_count')
8
- .order('job_count DESC')
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
13
+ @queue_stats = aggregate_queue_stats
10
14
 
11
- render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues, @paused_queues).render)
15
+ render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(
16
+ @queues, @paused_queues,
17
+ queue_stats: @queue_stats,
18
+ sort: sort_params
19
+ ).render)
12
20
  end
13
21
 
14
22
  def show
@@ -16,9 +24,9 @@ module SolidQueueMonitor
16
24
  @paused = QueuePauseService.paused_queues.include?(@queue_name)
17
25
 
18
26
  # Get all jobs for this queue with filtering and pagination
19
- base_query = SolidQueue::Job.where(queue_name: @queue_name).order(created_at: :desc)
20
- filtered_query = filter_queue_jobs(base_query)
21
- @jobs = paginate(filtered_query)
27
+ base_query = SolidQueue::Job.where(queue_name: @queue_name)
28
+ sorted_query = apply_sorting(filter_queue_jobs(base_query), QUEUE_DETAILS_SORTABLE_COLUMNS, 'created_at', :desc)
29
+ @jobs = paginate(sorted_query)
22
30
  preload_job_statuses(@jobs[:records])
23
31
 
24
32
  @counts = calculate_queue_counts(@queue_name)
@@ -31,7 +39,8 @@ module SolidQueueMonitor
31
39
  counts: @counts,
32
40
  current_page: @jobs[:current_page],
33
41
  total_pages: @jobs[:total_pages],
34
- filters: queue_filter_params
42
+ filters: queue_filter_params,
43
+ sort: sort_params
35
44
  ).render)
36
45
  end
37
46
 
@@ -53,6 +62,15 @@ module SolidQueueMonitor
53
62
 
54
63
  private
55
64
 
65
+ def aggregate_queue_stats
66
+ {
67
+ ready: SolidQueue::ReadyExecution.group(:queue_name).count,
68
+ scheduled: SolidQueue::ScheduledExecution.group(:queue_name).count,
69
+ failed: SolidQueue::FailedExecution.joins(:job)
70
+ .group('solid_queue_jobs.queue_name').count
71
+ }
72
+ end
73
+
56
74
  def calculate_queue_counts(queue_name)
57
75
  {
58
76
  total: SolidQueue::Job.where(queue_name: queue_name).count,
@@ -73,17 +91,13 @@ module SolidQueueMonitor
73
91
  when 'completed'
74
92
  relation = relation.where.not(finished_at: nil)
75
93
  when 'failed'
76
- failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
77
- relation = relation.where(id: failed_job_ids)
94
+ relation = relation.where(id: SolidQueue::FailedExecution.select(:job_id))
78
95
  when 'scheduled'
79
- scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
80
- relation = relation.where(id: scheduled_job_ids)
96
+ relation = relation.where(id: SolidQueue::ScheduledExecution.select(:job_id))
81
97
  when 'pending'
82
- ready_job_ids = SolidQueue::ReadyExecution.pluck(:job_id)
83
- relation = relation.where(id: ready_job_ids)
98
+ relation = relation.where(id: SolidQueue::ReadyExecution.select(:job_id))
84
99
  when 'in_progress'
85
- claimed_job_ids = SolidQueue::ClaimedExecution.pluck(:job_id)
86
- relation = relation.where(id: claimed_job_ids)
100
+ relation = relation.where(id: SolidQueue::ClaimedExecution.select(:job_id))
87
101
  end
88
102
  end
89
103
 
@@ -97,5 +111,14 @@ module SolidQueueMonitor
97
111
  status: params[:status]
98
112
  }
99
113
  end
114
+
115
+ def apply_queue_sorting(relation)
116
+ column = sort_params[:sort_by]
117
+ direction = sort_params[:sort_direction]
118
+ column = 'job_count' unless SORTABLE_COLUMNS.include?(column)
119
+ direction = 'desc' unless %w[asc desc].include?(direction)
120
+
121
+ relation.order("#{column} #{direction}")
122
+ end
100
123
  end
101
124
  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).order(created_at: :desc)
7
- @ready_jobs = paginate(filter_ready_jobs(base_query))
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).render)
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.order(:key))
7
- @recurring_jobs = paginate(base_query)
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).render)
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).order(scheduled_at: :asc)
7
- @scheduled_jobs = paginate(filter_scheduled_jobs(base_query))
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).render)
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.order(created_at: :desc)
7
- filtered_query = filter_workers(base_query)
8
- @processes = paginate(filtered_query)
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 ' &udarr;' unless is_active
135
+
136
+ @sort[:sort_direction] == 'asc' ? ' &uarr;' : ' &darr;'
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
- <th>Job</th>
64
- <th>Queue</th>
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
- <th>Job</th>
51
- <th>Queue</th>
51
+ #{sortable_header('class_name', 'Job')}
52
+ #{sortable_header('queue_name', 'Queue')}
52
53
  <th>Arguments</th>
53
- <th>Started At</th>
54
+ #{sortable_header('created_at', 'Started At')}
54
55
  <th>Process ID</th>
55
56
  </tr>
56
57
  </thead>