diff --git a/.rubocop.yml b/.rubocop.yml
index 9e3df3a..93c6c65 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -27,7 +27,7 @@ Metrics/ClassLength:
- 'app/presenters/solid_queue_monitor/job_details_presenter.rb'
Metrics/ParameterLists:
- Max: 7
+ Max: 8
Metrics/ModuleLength:
Max: 200
diff --git a/app/controllers/solid_queue_monitor/base_controller.rb b/app/controllers/solid_queue_monitor/base_controller.rb
index 19fe641..9275610 100644
--- a/app/controllers/solid_queue_monitor/base_controller.rb
+++ b/app/controllers/solid_queue_monitor/base_controller.rb
@@ -202,5 +202,36 @@ def filter_params
status: params[:status]
}
end
+
+ def sort_params
+ {
+ sort_by: params[:sort_by],
+ sort_direction: params[:sort_direction]
+ }
+ end
+
+ def apply_sorting(relation, allowed_columns, default_column, default_direction = :desc)
+ column = sort_params[:sort_by]
+ direction = sort_params[:sort_direction]
+ column = default_column unless allowed_columns.include?(column)
+ direction = %w[asc desc].include?(direction) ? direction.to_sym : default_direction
+ relation.order(column => direction)
+ end
+
+ def apply_execution_sorting(relation, allowed_columns, default_column, default_direction = :desc)
+ column = sort_params[:sort_by]
+ direction = sort_params[:sort_direction]
+ column = default_column unless allowed_columns.include?(column)
+ direction = %w[asc desc].include?(direction) ? direction.to_sym : default_direction
+
+ # Columns that exist on the jobs table, not on execution tables
+ job_table_columns = %w[class_name queue_name]
+
+ if job_table_columns.include?(column)
+ relation.joins(:job).order("solid_queue_jobs.#{column}" => direction)
+ else
+ relation.order(column => direction)
+ end
+ end
end
end
diff --git a/app/controllers/solid_queue_monitor/failed_jobs_controller.rb b/app/controllers/solid_queue_monitor/failed_jobs_controller.rb
index 0f3e608..696a887 100644
--- a/app/controllers/solid_queue_monitor/failed_jobs_controller.rb
+++ b/app/controllers/solid_queue_monitor/failed_jobs_controller.rb
@@ -2,14 +2,18 @@
module SolidQueueMonitor
class FailedJobsController < BaseController
+ SORTABLE_COLUMNS = %w[class_name queue_name created_at].freeze
+
def index
- base_query = SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
- @failed_jobs = paginate(filter_failed_jobs(base_query))
+ base_query = SolidQueue::FailedExecution.includes(:job)
+ sorted_query = apply_execution_sorting(filter_failed_jobs(base_query), SORTABLE_COLUMNS, 'created_at', :desc)
+ @failed_jobs = paginate(sorted_query)
render_page('Failed Jobs', SolidQueueMonitor::FailedJobsPresenter.new(@failed_jobs[:records],
current_page: @failed_jobs[:current_page],
total_pages: @failed_jobs[:total_pages],
- filters: filter_params).render)
+ filters: filter_params,
+ sort: sort_params).render)
end
def retry
diff --git a/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb b/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb
index 32f34d9..48e3bf5 100644
--- a/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb
+++ b/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb
@@ -2,14 +2,18 @@
module SolidQueueMonitor
class InProgressJobsController < BaseController
+ SORTABLE_COLUMNS = %w[class_name queue_name created_at].freeze
+
def index
- base_query = SolidQueue::ClaimedExecution.includes(:job).order(created_at: :desc)
- @in_progress_jobs = paginate(filter_in_progress_jobs(base_query))
+ base_query = SolidQueue::ClaimedExecution.includes(:job)
+ sorted_query = apply_execution_sorting(filter_in_progress_jobs(base_query), SORTABLE_COLUMNS, 'created_at', :desc)
+ @in_progress_jobs = paginate(sorted_query)
render_page('In Progress Jobs', SolidQueueMonitor::InProgressJobsPresenter.new(@in_progress_jobs[:records],
current_page: @in_progress_jobs[:current_page],
total_pages: @in_progress_jobs[:total_pages],
- filters: filter_params).render)
+ filters: filter_params,
+ sort: sort_params).render)
end
private
diff --git a/app/controllers/solid_queue_monitor/overview_controller.rb b/app/controllers/solid_queue_monitor/overview_controller.rb
index 5d88b48..3ade073 100644
--- a/app/controllers/solid_queue_monitor/overview_controller.rb
+++ b/app/controllers/solid_queue_monitor/overview_controller.rb
@@ -2,12 +2,15 @@
module SolidQueueMonitor
class OverviewController < BaseController
+ SORTABLE_COLUMNS = %w[class_name queue_name created_at].freeze
+
def index
@stats = SolidQueueMonitor::StatsCalculator.calculate
@chart_data = SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate
- recent_jobs_query = SolidQueue::Job.order(created_at: :desc).limit(100)
- @recent_jobs = paginate(filter_jobs(recent_jobs_query))
+ recent_jobs_query = SolidQueue::Job.limit(100)
+ sorted_query = apply_sorting(filter_jobs(recent_jobs_query), SORTABLE_COLUMNS, 'created_at', :desc)
+ @recent_jobs = paginate(sorted_query)
preload_job_statuses(@recent_jobs[:records])
@@ -31,7 +34,8 @@ def generate_overview_content
SolidQueueMonitor::JobsPresenter.new(@recent_jobs[:records],
current_page: @recent_jobs[:current_page],
total_pages: @recent_jobs[:total_pages],
- filters: filter_params).render
+ filters: filter_params,
+ sort: sort_params).render
end
end
end
diff --git a/app/controllers/solid_queue_monitor/queues_controller.rb b/app/controllers/solid_queue_monitor/queues_controller.rb
index f9ad5c6..2a3764d 100644
--- a/app/controllers/solid_queue_monitor/queues_controller.rb
+++ b/app/controllers/solid_queue_monitor/queues_controller.rb
@@ -2,13 +2,16 @@
module SolidQueueMonitor
class QueuesController < BaseController
+ SORTABLE_COLUMNS = %w[queue_name job_count].freeze
+ QUEUE_DETAILS_SORTABLE_COLUMNS = %w[class_name created_at].freeze
+
def index
- @queues = SolidQueue::Job.group(:queue_name)
- .select('queue_name, COUNT(*) as job_count')
- .order('job_count DESC')
+ base_query = SolidQueue::Job.group(:queue_name)
+ .select('queue_name, COUNT(*) as job_count')
+ @queues = apply_queue_sorting(base_query)
@paused_queues = QueuePauseService.paused_queues
- render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues, @paused_queues).render)
+ render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues, @paused_queues, sort: sort_params).render)
end
def show
@@ -16,9 +19,9 @@ def show
@paused = QueuePauseService.paused_queues.include?(@queue_name)
# Get all jobs for this queue with filtering and pagination
- base_query = SolidQueue::Job.where(queue_name: @queue_name).order(created_at: :desc)
- filtered_query = filter_queue_jobs(base_query)
- @jobs = paginate(filtered_query)
+ base_query = SolidQueue::Job.where(queue_name: @queue_name)
+ sorted_query = apply_sorting(filter_queue_jobs(base_query), QUEUE_DETAILS_SORTABLE_COLUMNS, 'created_at', :desc)
+ @jobs = paginate(sorted_query)
preload_job_statuses(@jobs[:records])
@counts = calculate_queue_counts(@queue_name)
@@ -31,7 +34,8 @@ def show
counts: @counts,
current_page: @jobs[:current_page],
total_pages: @jobs[:total_pages],
- filters: queue_filter_params
+ filters: queue_filter_params,
+ sort: sort_params
).render)
end
@@ -97,5 +101,14 @@ def queue_filter_params
status: params[:status]
}
end
+
+ def apply_queue_sorting(relation)
+ column = sort_params[:sort_by]
+ direction = sort_params[:sort_direction]
+ column = 'job_count' unless SORTABLE_COLUMNS.include?(column)
+ direction = 'desc' unless %w[asc desc].include?(direction)
+
+ relation.order("#{column} #{direction}")
+ end
end
end
diff --git a/app/controllers/solid_queue_monitor/ready_jobs_controller.rb b/app/controllers/solid_queue_monitor/ready_jobs_controller.rb
index 4448f3f..9ba4b98 100644
--- a/app/controllers/solid_queue_monitor/ready_jobs_controller.rb
+++ b/app/controllers/solid_queue_monitor/ready_jobs_controller.rb
@@ -2,14 +2,18 @@
module SolidQueueMonitor
class ReadyJobsController < BaseController
+ SORTABLE_COLUMNS = %w[class_name queue_name priority created_at].freeze
+
def index
- base_query = SolidQueue::ReadyExecution.includes(:job).order(created_at: :desc)
- @ready_jobs = paginate(filter_ready_jobs(base_query))
+ base_query = SolidQueue::ReadyExecution.includes(:job)
+ sorted_query = apply_execution_sorting(filter_ready_jobs(base_query), SORTABLE_COLUMNS, 'created_at', :desc)
+ @ready_jobs = paginate(sorted_query)
render_page('Ready Jobs', SolidQueueMonitor::ReadyJobsPresenter.new(@ready_jobs[:records],
current_page: @ready_jobs[:current_page],
total_pages: @ready_jobs[:total_pages],
- filters: filter_params).render)
+ filters: filter_params,
+ sort: sort_params).render)
end
end
end
diff --git a/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb b/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb
index ba55b7d..6e5121a 100644
--- a/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb
+++ b/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb
@@ -2,14 +2,18 @@
module SolidQueueMonitor
class RecurringJobsController < BaseController
+ SORTABLE_COLUMNS = %w[key class_name queue_name priority].freeze
+
def index
- base_query = filter_recurring_jobs(SolidQueue::RecurringTask.order(:key))
- @recurring_jobs = paginate(base_query)
+ base_query = filter_recurring_jobs(SolidQueue::RecurringTask.all)
+ sorted_query = apply_sorting(base_query, SORTABLE_COLUMNS, 'key', :asc)
+ @recurring_jobs = paginate(sorted_query)
render_page('Recurring Jobs', SolidQueueMonitor::RecurringJobsPresenter.new(@recurring_jobs[:records],
current_page: @recurring_jobs[:current_page],
total_pages: @recurring_jobs[:total_pages],
- filters: filter_params).render)
+ filters: filter_params,
+ sort: sort_params).render)
end
end
end
diff --git a/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb b/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb
index 7409476..bacfc6e 100644
--- a/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb
+++ b/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb
@@ -2,14 +2,18 @@
module SolidQueueMonitor
class ScheduledJobsController < BaseController
+ SORTABLE_COLUMNS = %w[class_name queue_name scheduled_at].freeze
+
def index
- base_query = SolidQueue::ScheduledExecution.includes(:job).order(scheduled_at: :asc)
- @scheduled_jobs = paginate(filter_scheduled_jobs(base_query))
+ base_query = SolidQueue::ScheduledExecution.includes(:job)
+ sorted_query = apply_execution_sorting(filter_scheduled_jobs(base_query), SORTABLE_COLUMNS, 'scheduled_at', :asc)
+ @scheduled_jobs = paginate(sorted_query)
render_page('Scheduled Jobs', SolidQueueMonitor::ScheduledJobsPresenter.new(@scheduled_jobs[:records],
current_page: @scheduled_jobs[:current_page],
total_pages: @scheduled_jobs[:total_pages],
- filters: filter_params).render)
+ filters: filter_params,
+ sort: sort_params).render)
end
def create
diff --git a/app/controllers/solid_queue_monitor/workers_controller.rb b/app/controllers/solid_queue_monitor/workers_controller.rb
index 7fe0040..fdfbdee 100644
--- a/app/controllers/solid_queue_monitor/workers_controller.rb
+++ b/app/controllers/solid_queue_monitor/workers_controller.rb
@@ -2,16 +2,19 @@
module SolidQueueMonitor
class WorkersController < BaseController
+ SORTABLE_COLUMNS = %w[hostname last_heartbeat_at].freeze
+
def index
- base_query = SolidQueue::Process.order(created_at: :desc)
- filtered_query = filter_workers(base_query)
- @processes = paginate(filtered_query)
+ base_query = SolidQueue::Process.all
+ sorted_query = apply_sorting(filter_workers(base_query), SORTABLE_COLUMNS, 'last_heartbeat_at', :desc)
+ @processes = paginate(sorted_query)
render_page('Workers', SolidQueueMonitor::WorkersPresenter.new(
@processes[:records],
current_page: @processes[:current_page],
total_pages: @processes[:total_pages],
- filters: worker_filter_params
+ filters: worker_filter_params,
+ sort: sort_params
).render)
end
diff --git a/app/presenters/solid_queue_monitor/base_presenter.rb b/app/presenters/solid_queue_monitor/base_presenter.rb
index 83d06c5..61d07df 100644
--- a/app/presenters/solid_queue_monitor/base_presenter.rb
+++ b/app/presenters/solid_queue_monitor/base_presenter.rb
@@ -118,6 +118,34 @@ def queue_link(queue_name, css_class: nil)
"#{queue_name}"
end
+ def sortable_header(column, label)
+ return "
#{label} | " unless @sort
+
+ column_str = column.to_s
+ is_active = @sort[:sort_by] == column_str
+ next_direction = is_active && @sort[:sort_direction] == 'asc' ? 'desc' : 'asc'
+ arrow = sort_arrow(is_active)
+ css_class = is_active ? 'sortable-header active' : 'sortable-header'
+
+ "#{label}#{arrow} | "
+ end
+
+ def sort_arrow(is_active)
+ return ' ⇅' unless is_active
+
+ @sort[:sort_direction] == 'asc' ? ' ↑' : ' ↓'
+ end
+
+ def filter_query_string
+ params = []
+ params << "class_name=#{@filters[:class_name]}" if @filters && @filters[:class_name].present?
+ params << "queue_name=#{@filters[:queue_name]}" if @filters && @filters[:queue_name].present?
+ params << "arguments=#{@filters[:arguments]}" if @filters && @filters[:arguments].present?
+ params << "status=#{@filters[:status]}" if @filters && @filters[:status].present?
+
+ params.empty? ? '' : "{params.join('&')}"
+ end
+
def request_path
if defined?(controller) && controller.respond_to?(:request)
controller.request.path
@@ -138,14 +166,28 @@ def engine_mount_point
private
def query_params
- params = []
- params << "class_name=#{@filters[:class_name]}" if @filters && @filters[:class_name].present?
- params << "queue_name=#{@filters[:queue_name]}" if @filters && @filters[:queue_name].present?
- params << "status=#{@filters[:status]}" if @filters && @filters[:status].present?
-
+ params = build_filter_params + build_sort_params
params.empty? ? '' : "{params.join('&')}"
end
+ def build_filter_params
+ return [] unless @filters
+
+ filter_keys = %i[class_name queue_name status]
+ filter_keys.filter_map do |key|
+ "#{key}=#{@filters[key]}" if @filters[key].present?
+ end
+ end
+
+ def build_sort_params
+ return [] unless @sort
+
+ sort_keys = %i[sort_by sort_direction]
+ sort_keys.filter_map do |key|
+ "#{key}=#{@sort[key]}" if @sort[key].present?
+ end
+ end
+
def full_path(route_name, *args)
SolidQueueMonitor::Engine.routes.url_helpers.send(route_name, *args)
rescue NoMethodError
diff --git a/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb b/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb
index 1b24e36..cef39e8 100644
--- a/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb
+++ b/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb
@@ -5,11 +5,12 @@ class FailedJobsPresenter < BasePresenter
include Rails.application.routes.url_helpers
include SolidQueueMonitor::Engine.routes.url_helpers
- def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
+ def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
@jobs = jobs
@current_page = current_page
@total_pages = total_pages
@filters = filters
+ @sort = sort
end
def render
@@ -60,10 +61,11 @@ def generate_table
|
- Job |
- Queue |
+ #{sortable_header('class_name', 'Job')}
+ #{sortable_header('queue_name', 'Queue')}
Error |
Arguments |
+ #{sortable_header('created_at', 'Failed At')}
Actions |
@@ -261,11 +263,9 @@ def generate_row(failed_execution)
#{error[:message].to_s.truncate(100)}
-
- Failed at: #{format_datetime(failed_execution.created_at)}
-
|
#{format_arguments(job.arguments)} |
+ #{format_datetime(failed_execution.created_at)} |
- | Job |
- Queue |
+ #{sortable_header('class_name', 'Job')}
+ #{sortable_header('queue_name', 'Queue')}
Arguments |
- Started At |
+ #{sortable_header('created_at', 'Started At')}
Process ID |
diff --git a/app/presenters/solid_queue_monitor/jobs_presenter.rb b/app/presenters/solid_queue_monitor/jobs_presenter.rb
index e7216da..746ca8f 100644
--- a/app/presenters/solid_queue_monitor/jobs_presenter.rb
+++ b/app/presenters/solid_queue_monitor/jobs_presenter.rb
@@ -5,11 +5,12 @@ class JobsPresenter < BasePresenter
include Rails.application.routes.url_helpers
include SolidQueueMonitor::Engine.routes.url_helpers
- def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
+ def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
@jobs = jobs
@current_page = current_page
@total_pages = total_pages
@filters = filters
+ @sort = sort
end
def render
@@ -73,11 +74,11 @@ def generate_table
| ID |
- Job |
- Queue |
+ #{sortable_header('class_name', 'Job')}
+ #{sortable_header('queue_name', 'Queue')}
Arguments |
Status |
- Created At |
+ #{sortable_header('created_at', 'Created At')}
Actions |
diff --git a/app/presenters/solid_queue_monitor/queue_details_presenter.rb b/app/presenters/solid_queue_monitor/queue_details_presenter.rb
index beb1931..c36caa0 100644
--- a/app/presenters/solid_queue_monitor/queue_details_presenter.rb
+++ b/app/presenters/solid_queue_monitor/queue_details_presenter.rb
@@ -2,7 +2,7 @@
module SolidQueueMonitor
class QueueDetailsPresenter < BasePresenter
- def initialize(queue_name:, paused:, jobs:, counts:, current_page: 1, total_pages: 1, filters: {})
+ def initialize(queue_name:, paused:, jobs:, counts:, current_page: 1, total_pages: 1, filters: {}, sort: {})
@queue_name = queue_name
@paused = paused
@jobs = jobs
@@ -10,6 +10,7 @@ def initialize(queue_name:, paused:, jobs:, counts:, current_page: 1, total_page
@current_page = current_page
@total_pages = total_pages
@filters = filters
+ @sort = sort
end
def render
@@ -129,10 +130,10 @@ def generate_table
| ID |
- Job |
+ #{sortable_header('class_name', 'Job')}
Arguments |
Status |
- Created At |
+ #{sortable_header('created_at', 'Created At')}
Actions |
diff --git a/app/presenters/solid_queue_monitor/queues_presenter.rb b/app/presenters/solid_queue_monitor/queues_presenter.rb
index 13ff926..ed5152a 100644
--- a/app/presenters/solid_queue_monitor/queues_presenter.rb
+++ b/app/presenters/solid_queue_monitor/queues_presenter.rb
@@ -2,9 +2,10 @@
module SolidQueueMonitor
class QueuesPresenter < BasePresenter
- def initialize(records, paused_queues = [])
+ def initialize(records, paused_queues = [], sort: {})
@records = records
@paused_queues = paused_queues
+ @sort = sort
end
def render
@@ -19,9 +20,9 @@ def generate_table
- | Queue Name |
+ #{sortable_header('queue_name', 'Queue Name')}
Status |
- Total Jobs |
+ #{sortable_header('job_count', 'Total Jobs')}
Ready Jobs |
Scheduled Jobs |
Failed Jobs |
diff --git a/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb b/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb
index 6e62c8b..d3557ad 100644
--- a/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb
+++ b/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb
@@ -2,11 +2,12 @@
module SolidQueueMonitor
class ReadyJobsPresenter < BasePresenter
- def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
+ def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
@jobs = jobs
@current_page = current_page
@total_pages = total_pages
@filters = filters
+ @sort = sort
end
def render
@@ -50,11 +51,11 @@ def generate_table
- | Job |
- Queue |
- Priority |
+ #{sortable_header('class_name', 'Job')}
+ #{sortable_header('queue_name', 'Queue')}
+ #{sortable_header('priority', 'Priority')}
Arguments |
- Created At |
+ #{sortable_header('created_at', 'Created At')}
diff --git a/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb b/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb
index a771f69..ecbefe6 100644
--- a/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb
+++ b/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb
@@ -5,11 +5,12 @@ class RecurringJobsPresenter < BasePresenter
include Rails.application.routes.url_helpers
include SolidQueueMonitor::Engine.routes.url_helpers
- def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
+ def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
@jobs = jobs
@current_page = current_page
@total_pages = total_pages
@filters = filters
+ @sort = sort
end
def render
@@ -48,11 +49,11 @@ def generate_table
- | Key |
- Job |
+ #{sortable_header('key', 'Key')}
+ #{sortable_header('class_name', 'Job')}
Schedule |
- Queue |
- Priority |
+ #{sortable_header('queue_name', 'Queue')}
+ #{sortable_header('priority', 'Priority')}
Last Updated |
diff --git a/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb b/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb
index 051d29c..a1283eb 100644
--- a/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb
+++ b/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb
@@ -5,11 +5,12 @@ class ScheduledJobsPresenter < BasePresenter
include Rails.application.routes.url_helpers
include SolidQueueMonitor::Engine.routes.url_helpers
- def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
+ def initialize(jobs, current_page: 1, total_pages: 1, filters: {}, sort: {})
@jobs = jobs
@current_page = current_page
@total_pages = total_pages
@filters = filters
+ @sort = sort
end
def render
@@ -140,9 +141,9 @@ def generate_table
|
- Job |
- Queue |
- Scheduled At |
+ #{sortable_header('class_name', 'Job')}
+ #{sortable_header('queue_name', 'Queue')}
+ #{sortable_header('scheduled_at', 'Scheduled At')}
Arguments |
diff --git a/app/presenters/solid_queue_monitor/workers_presenter.rb b/app/presenters/solid_queue_monitor/workers_presenter.rb
index de32a4e..873f31e 100644
--- a/app/presenters/solid_queue_monitor/workers_presenter.rb
+++ b/app/presenters/solid_queue_monitor/workers_presenter.rb
@@ -5,11 +5,12 @@ class WorkersPresenter < BasePresenter
HEARTBEAT_STALE_THRESHOLD = 5.minutes
HEARTBEAT_DEAD_THRESHOLD = 10.minutes
- def initialize(processes, current_page: 1, total_pages: 1, filters: {})
+ def initialize(processes, current_page: 1, total_pages: 1, filters: {}, sort: {})
@processes = processes.to_a # Load records once to avoid multiple queries
@current_page = current_page
@total_pages = total_pages
@filters = filters
+ @sort = sort
preload_claimed_data
calculate_summary_stats
end
@@ -140,10 +141,10 @@ def generate_table
| Kind |
- Hostname |
+ #{sortable_header('hostname', 'Hostname')}
PID |
Queues |
- Last Heartbeat |
+ #{sortable_header('last_heartbeat_at', 'Last Heartbeat')}
Status |
Jobs Processing |
Actions |
diff --git a/app/services/solid_queue_monitor/stylesheet_generator.rb b/app/services/solid_queue_monitor/stylesheet_generator.rb
index 6b5e242..862165c 100644
--- a/app/services/solid_queue_monitor/stylesheet_generator.rb
+++ b/app/services/solid_queue_monitor/stylesheet_generator.rb
@@ -205,6 +205,22 @@ def generate
color: var(--text-muted);
}
+ .solid_queue_monitor .sortable-header {
+ color: var(--text-muted);
+ text-decoration: none;
+ cursor: pointer;
+ transition: color 0.2s;
+ }
+
+ .solid_queue_monitor .sortable-header:hover {
+ color: var(--primary-color);
+ }
+
+ .solid_queue_monitor .sortable-header.active {
+ color: var(--primary-color);
+ font-weight: 600;
+ }
+
.solid_queue_monitor .status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
diff --git a/spec/requests/solid_queue_monitor/sorting_spec.rb b/spec/requests/solid_queue_monitor/sorting_spec.rb
new file mode 100644
index 0000000..d452c2b
--- /dev/null
+++ b/spec/requests/solid_queue_monitor/sorting_spec.rb
@@ -0,0 +1,255 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Sorting' do
+ describe 'Ready Jobs sorting' do
+ let!(:job_a) { create(:solid_queue_job, class_name: 'AJob', queue_name: 'default', created_at: 2.hours.ago) }
+ let!(:job_b) { create(:solid_queue_job, class_name: 'BJob', queue_name: 'high', created_at: 1.hour.ago) }
+ let!(:ready_a) { create(:solid_queue_ready_execution, job: job_a, queue_name: 'default', priority: 10) }
+ let!(:ready_b) { create(:solid_queue_ready_execution, job: job_b, queue_name: 'high', priority: 5) }
+
+ it 'sorts by class_name ascending' do
+ get '/ready_jobs', params: { sort_by: 'class_name', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('AJob')).to be < response.body.index('BJob')
+ end
+
+ it 'sorts by class_name descending' do
+ get '/ready_jobs', params: { sort_by: 'class_name', sort_direction: 'desc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('BJob')).to be < response.body.index('AJob')
+ end
+
+ it 'sorts by queue_name' do
+ get '/ready_jobs', params: { sort_by: 'queue_name', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('default')).to be < response.body.index('high')
+ end
+
+ it 'sorts by priority' do
+ get '/ready_jobs', params: { sort_by: 'priority', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ # Priority 5 should come before priority 10
+ expect(response.body.index('BJob')).to be < response.body.index('AJob')
+ end
+
+ it 'uses default sort when invalid column provided' do
+ get '/ready_jobs', params: { sort_by: 'invalid_column', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'shows sort indicator arrow' do
+ get '/ready_jobs', params: { sort_by: 'class_name', sort_direction: 'asc' }
+
+ expect(response.body).to include('↑')
+ end
+
+ it 'shows descending arrow when sorting descending' do
+ get '/ready_jobs', params: { sort_by: 'class_name', sort_direction: 'desc' }
+
+ expect(response.body).to include('↓')
+ end
+
+ it 'includes sortable header links' do
+ get '/ready_jobs'
+
+ expect(response.body).to include('sortable-header')
+ expect(response.body).to include('sort_by=class_name')
+ end
+
+ it 'shows default sort indicator on unsorted columns' do
+ get '/ready_jobs'
+
+ expect(response.body).to include('⇅')
+ end
+ end
+
+ describe 'Scheduled Jobs sorting' do
+ let!(:job_a) { create(:solid_queue_job, class_name: 'AJob') }
+ let!(:job_b) { create(:solid_queue_job, class_name: 'BJob') }
+ let!(:scheduled_a) { create(:solid_queue_scheduled_execution, job: job_a, scheduled_at: 2.hours.from_now) }
+ let!(:scheduled_b) { create(:solid_queue_scheduled_execution, job: job_b, scheduled_at: 1.hour.from_now) }
+
+ it 'sorts by scheduled_at ascending by default' do
+ get '/scheduled_jobs'
+
+ expect(response).to have_http_status(:ok)
+ # Earlier scheduled time should come first
+ expect(response.body.index('BJob')).to be < response.body.index('AJob')
+ end
+
+ it 'sorts by scheduled_at descending' do
+ get '/scheduled_jobs', params: { sort_by: 'scheduled_at', sort_direction: 'desc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('AJob')).to be < response.body.index('BJob')
+ end
+
+ it 'sorts by class_name' do
+ get '/scheduled_jobs', params: { sort_by: 'class_name', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('AJob')).to be < response.body.index('BJob')
+ end
+ end
+
+ describe 'Failed Jobs sorting' do
+ let!(:job_a) { create(:solid_queue_job, class_name: 'AFailedJob', queue_name: 'default') }
+ let!(:job_b) { create(:solid_queue_job, class_name: 'BFailedJob', queue_name: 'high') }
+ let!(:failed_a) { create(:solid_queue_failed_execution, job: job_a, created_at: 2.hours.ago) }
+ let!(:failed_b) { create(:solid_queue_failed_execution, job: job_b, created_at: 1.hour.ago) }
+
+ it 'sorts by created_at descending by default' do
+ get '/failed_jobs'
+
+ expect(response).to have_http_status(:ok)
+ # More recent should come first
+ expect(response.body.index('BFailedJob')).to be < response.body.index('AFailedJob')
+ end
+
+ it 'sorts by class_name ascending' do
+ get '/failed_jobs', params: { sort_by: 'class_name', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('AFailedJob')).to be < response.body.index('BFailedJob')
+ end
+
+ it 'sorts by queue_name' do
+ get '/failed_jobs', params: { sort_by: 'queue_name', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ describe 'Recurring Jobs sorting' do
+ before do
+ # Create recurring tasks directly
+ SolidQueue::RecurringTask.create!(key: 'a_task', class_name: 'ARecurringJob', queue_name: 'default', schedule: '0 * * * *')
+ SolidQueue::RecurringTask.create!(key: 'b_task', class_name: 'BRecurringJob', queue_name: 'high', schedule: '0 * * * *')
+ end
+
+ it 'sorts by key ascending by default' do
+ get '/recurring_jobs'
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('a_task')).to be < response.body.index('b_task')
+ end
+
+ it 'sorts by key descending' do
+ get '/recurring_jobs', params: { sort_by: 'key', sort_direction: 'desc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('b_task')).to be < response.body.index('a_task')
+ end
+
+ it 'sorts by class_name' do
+ get '/recurring_jobs', params: { sort_by: 'class_name', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('ARecurringJob')).to be < response.body.index('BRecurringJob')
+ end
+ end
+
+ describe 'Workers sorting' do
+ let!(:worker_a) { create(:solid_queue_process, hostname: 'alpha-host', last_heartbeat_at: 1.minute.ago) }
+ let!(:worker_b) { create(:solid_queue_process, hostname: 'beta-host', last_heartbeat_at: 2.minutes.ago) }
+
+ it 'sorts by last_heartbeat_at descending by default' do
+ get '/workers'
+
+ expect(response).to have_http_status(:ok)
+ # More recent heartbeat should come first
+ expect(response.body.index('alpha-host')).to be < response.body.index('beta-host')
+ end
+
+ it 'sorts by hostname ascending' do
+ get '/workers', params: { sort_by: 'hostname', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('alpha-host')).to be < response.body.index('beta-host')
+ end
+
+ it 'sorts by hostname descending' do
+ get '/workers', params: { sort_by: 'hostname', sort_direction: 'desc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('beta-host')).to be < response.body.index('alpha-host')
+ end
+ end
+
+ describe 'Overview page sorting' do
+ let!(:job_a) { create(:solid_queue_job, class_name: 'AOverviewJob', created_at: 2.hours.ago) }
+ let!(:job_b) { create(:solid_queue_job, class_name: 'BOverviewJob', created_at: 1.hour.ago) }
+
+ it 'sorts by created_at descending by default' do
+ get '/'
+
+ expect(response).to have_http_status(:ok)
+ # More recent should come first
+ expect(response.body.index('BOverviewJob')).to be < response.body.index('AOverviewJob')
+ end
+
+ it 'sorts by class_name ascending' do
+ get '/', params: { sort_by: 'class_name', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('AOverviewJob')).to be < response.body.index('BOverviewJob')
+ end
+ end
+
+ describe 'Queue details sorting' do
+ let!(:job_a) { create(:solid_queue_job, class_name: 'AQueueJob', queue_name: 'test_queue', created_at: 2.hours.ago) }
+ let!(:job_b) { create(:solid_queue_job, class_name: 'BQueueJob', queue_name: 'test_queue', created_at: 1.hour.ago) }
+
+ it 'sorts by created_at descending by default' do
+ get '/queues/test_queue'
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('BQueueJob')).to be < response.body.index('AQueueJob')
+ end
+
+ it 'sorts by class_name ascending' do
+ get '/queues/test_queue', params: { sort_by: 'class_name', sort_direction: 'asc' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body.index('AQueueJob')).to be < response.body.index('BQueueJob')
+ end
+ end
+
+ describe 'Sorting with filters preserved' do
+ let!(:job) { create(:solid_queue_job, class_name: 'FilteredJob', queue_name: 'filtered_queue') }
+ let!(:ready) { create(:solid_queue_ready_execution, job: job, queue_name: 'filtered_queue') }
+
+ it 'preserves filters when sorting' do
+ get '/ready_jobs', params: { sort_by: 'class_name', sort_direction: 'asc', class_name: 'Filtered' }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include('FilteredJob')
+ expect(response.body).to include('class_name=Filtered')
+ end
+ end
+
+ describe 'Sorting with pagination' do
+ before do
+ # Create more jobs than a single page
+ 12.times do |i|
+ job = create(:solid_queue_job, class_name: "Job#{format('%02d', i)}")
+ create(:solid_queue_ready_execution, job: job)
+ end
+ end
+
+ it 'preserves sorting when navigating pages' do
+ get '/ready_jobs', params: { sort_by: 'class_name', sort_direction: 'asc', page: 2 }
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include('sort_by=class_name')
+ expect(response.body).to include('sort_direction=asc')
+ end
+ end
+end
|