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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/app/controllers/solid_queue_monitor/base_controller.rb +34 -2
  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 +7 -3
  6. data/app/controllers/solid_queue_monitor/overview_controller.rb +7 -3
  7. data/app/controllers/solid_queue_monitor/queues_controller.rb +21 -8
  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 +4 -3
  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/workers_presenter.rb +4 -3
  24. data/app/services/solid_queue_monitor/html_generator.rb +23 -2
  25. data/app/services/solid_queue_monitor/search_service.rb +126 -0
  26. data/app/services/solid_queue_monitor/stylesheet_generator.rb +118 -0
  27. data/config/routes.rb +1 -0
  28. data/lib/solid_queue_monitor/version.rb +1 -1
  29. 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: 1014e5b1b9afceff146efc3a81849dd12866578198b8172452d9b6edf42d7786
4
+ data.tar.gz: 0b2c11f5ba47a7e977c93cdb7a048c91d092b9ea97d72af27e908f6f24387fbf
5
5
  SHA512:
6
- metadata.gz: fa09c1f8f00e1add829d6c77dc9acc81833ce247c504b4c0dd3f916b2fd399e6684c6b5dc9ceb1942ea19a42219ef2b2311e42fe069ece66cf897dc3062fa0d5
7
- data.tar.gz: '079cffeabf96b8f22fef6a41acf074aba277ee6e65faf8a148f80b66ea8bad935cc80363f355b48dcd41200d2b360791a64dc7c2d0377882bce02ed12352e1f4'
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.0.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).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
@@ -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.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
 
@@ -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).render
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
- @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
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).order(created_at: :desc)
20
- filtered_query = filter_queue_jobs(base_query)
21
- @jobs = paginate(filtered_query)
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).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>
@@ -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
- <th>Job</th>
77
- <th>Queue</th>
77
+ #{sortable_header('class_name', 'Job')}
78
+ #{sortable_header('queue_name', 'Queue')}
78
79
  <th>Arguments</th>
79
80
  <th>Status</th>
80
- <th>Created At</th>
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
- <th>Job</th>
133
+ #{sortable_header('class_name', 'Job')}
133
134
  <th>Arguments</th>
134
135
  <th>Status</th>
135
- <th>Created At</th>
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
- <th>Queue Name</th>
23
+ #{sortable_header('queue_name', 'Queue Name')}
23
24
  <th>Status</th>
24
- <th>Total Jobs</th>
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
- <th>Job</th>
54
- <th>Queue</th>
55
- <th>Priority</th>
54
+ #{sortable_header('class_name', 'Job')}
55
+ #{sortable_header('queue_name', 'Queue')}
56
+ #{sortable_header('priority', 'Priority')}
56
57
  <th>Arguments</th>
57
- <th>Created At</th>
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
- <th>Key</th>
52
- <th>Job</th>
52
+ #{sortable_header('key', 'Key')}
53
+ #{sortable_header('class_name', 'Job')}
53
54
  <th>Schedule</th>
54
- <th>Queue</th>
55
- <th>Priority</th>
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
- <th>Job</th>
144
- <th>Queue</th>
145
- <th>Scheduled At</th>
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('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
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
- <th>Hostname</th>
144
+ #{sortable_header('hostname', 'Hostname')}
144
145
  <th>PID</th>
145
146
  <th>Queues</th>
146
- <th>Last Heartbeat</th>
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('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
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]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueMonitor
4
- VERSION = '1.0.1'
4
+ VERSION = '1.1.0'
5
5
  end
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.1
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: 3.6.7
122
+ rubygems_version: 4.0.6
120
123
  specification_version: 4
121
124
  summary: Simple monitoring interface for Solid Queue
122
125
  test_files: []