solid_queue_monitor 1.3.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +66 -4
- data/app/assets/javascripts/solid_queue_monitor/application.js +393 -0
- data/app/{services/solid_queue_monitor/stylesheet_generator.rb → assets/stylesheets/solid_queue_monitor/application.css} +23 -12
- data/app/controllers/solid_queue_monitor/application_controller.rb +9 -3
- data/app/controllers/solid_queue_monitor/assets_controller.rb +52 -0
- data/app/controllers/solid_queue_monitor/base_controller.rb +0 -29
- data/app/controllers/solid_queue_monitor/failed_jobs_controller.rb +3 -7
- data/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb +3 -6
- data/app/controllers/solid_queue_monitor/jobs_controller.rb +3 -7
- data/app/controllers/solid_queue_monitor/overview_controller.rb +3 -12
- data/app/controllers/solid_queue_monitor/queues_controller.rb +4 -18
- data/app/controllers/solid_queue_monitor/ready_jobs_controller.rb +3 -6
- data/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb +3 -6
- data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +3 -7
- data/app/controllers/solid_queue_monitor/search_controller.rb +3 -4
- data/app/controllers/solid_queue_monitor/workers_controller.rb +24 -8
- data/app/helpers/solid_queue_monitor/application_helper.rb +46 -0
- data/app/helpers/solid_queue_monitor/chart_helper.rb +293 -0
- data/app/helpers/solid_queue_monitor/job_details_helper.rb +66 -0
- data/app/helpers/solid_queue_monitor/jobs_helper.rb +134 -0
- data/app/helpers/solid_queue_monitor/pagination_helper.rb +23 -0
- data/app/helpers/solid_queue_monitor/sort_helper.rb +30 -0
- data/app/helpers/solid_queue_monitor/workers_helper.rb +88 -0
- data/app/services/solid_queue_monitor/asset_cache.rb +56 -0
- data/app/views/layouts/solid_queue_monitor/application.html.erb +25 -0
- data/app/views/solid_queue_monitor/failed_jobs/_row.html.erb +26 -0
- data/app/views/solid_queue_monitor/failed_jobs/index.html.erb +38 -0
- data/app/views/solid_queue_monitor/in_progress_jobs/_row.html.erb +13 -0
- data/app/views/solid_queue_monitor/in_progress_jobs/index.html.erb +25 -0
- data/app/views/solid_queue_monitor/jobs/_arguments.html.erb +9 -0
- data/app/views/solid_queue_monitor/jobs/_error.html.erb +26 -0
- data/app/views/solid_queue_monitor/jobs/_execution_history.html.erb +25 -0
- data/app/views/solid_queue_monitor/jobs/_header.html.erb +37 -0
- data/app/views/solid_queue_monitor/jobs/_metadata.html.erb +22 -0
- data/app/views/solid_queue_monitor/jobs/_raw_data.html.erb +11 -0
- data/app/views/solid_queue_monitor/jobs/_timeline.html.erb +29 -0
- data/app/views/solid_queue_monitor/jobs/_timing.html.erb +9 -0
- data/app/views/solid_queue_monitor/jobs/_worker.html.erb +12 -0
- data/app/views/solid_queue_monitor/jobs/show.html.erb +22 -0
- data/app/views/solid_queue_monitor/overview/_chart.html.erb +1 -0
- data/app/views/solid_queue_monitor/overview/_recent_job_row.html.erb +26 -0
- data/app/views/solid_queue_monitor/overview/_recent_jobs.html.erb +31 -0
- data/app/views/solid_queue_monitor/overview/_stat_card.html.erb +4 -0
- data/app/views/solid_queue_monitor/overview/_stats.html.erb +11 -0
- data/app/views/solid_queue_monitor/overview/index.html.erb +9 -0
- data/app/views/solid_queue_monitor/queues/_job_row.html.erb +26 -0
- data/app/views/solid_queue_monitor/queues/_row.html.erb +33 -0
- data/app/views/solid_queue_monitor/queues/index.html.erb +18 -0
- data/app/views/solid_queue_monitor/queues/show.html.erb +63 -0
- data/app/views/solid_queue_monitor/ready_jobs/_row.html.erb +7 -0
- data/app/views/solid_queue_monitor/ready_jobs/index.html.erb +25 -0
- data/app/views/solid_queue_monitor/recurring_jobs/_row.html.erb +8 -0
- data/app/views/solid_queue_monitor/recurring_jobs/index.html.erb +26 -0
- data/app/views/solid_queue_monitor/scheduled_jobs/_row.html.erb +7 -0
- data/app/views/solid_queue_monitor/scheduled_jobs/index.html.erb +36 -0
- data/app/views/solid_queue_monitor/search/_completed_row.html.erb +6 -0
- data/app/views/solid_queue_monitor/search/_failed_row.html.erb +6 -0
- data/app/views/solid_queue_monitor/search/_job_row.html.erb +9 -0
- data/app/views/solid_queue_monitor/search/_recurring_row.html.erb +6 -0
- data/app/views/solid_queue_monitor/search/_section.html.erb +25 -0
- data/app/views/solid_queue_monitor/search/index.html.erb +23 -0
- data/app/views/solid_queue_monitor/shared/_filters.html.erb +48 -0
- data/app/views/solid_queue_monitor/shared/_flash.html.erb +17 -0
- data/app/views/solid_queue_monitor/shared/_footer.html.erb +3 -0
- data/app/views/solid_queue_monitor/shared/_header.html.erb +81 -0
- data/app/views/solid_queue_monitor/shared/_jobs_table.html.erb +20 -0
- data/app/views/solid_queue_monitor/shared/_pagination.html.erb +25 -0
- data/app/views/solid_queue_monitor/workers/_row.html.erb +22 -0
- data/app/views/solid_queue_monitor/workers/index.html.erb +82 -0
- data/config/routes.rb +6 -1
- data/lib/solid_queue_monitor/engine.rb +2 -0
- data/lib/solid_queue_monitor/version.rb +1 -1
- data/lib/solid_queue_monitor.rb +8 -1
- metadata +57 -17
- data/app/presenters/solid_queue_monitor/base_presenter.rb +0 -211
- data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +0 -225
- data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +0 -84
- data/app/presenters/solid_queue_monitor/job_details_presenter.rb +0 -707
- data/app/presenters/solid_queue_monitor/jobs_presenter.rb +0 -144
- data/app/presenters/solid_queue_monitor/queue_details_presenter.rb +0 -195
- data/app/presenters/solid_queue_monitor/queues_presenter.rb +0 -89
- data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +0 -81
- data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +0 -81
- data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +0 -178
- data/app/presenters/solid_queue_monitor/search_results_presenter.rb +0 -190
- data/app/presenters/solid_queue_monitor/stats_presenter.rb +0 -36
- data/app/presenters/solid_queue_monitor/workers_presenter.rb +0 -325
- data/app/services/solid_queue_monitor/chart_presenter.rb +0 -239
- data/app/services/solid_queue_monitor/html_generator.rb +0 -427
|
@@ -6,35 +6,6 @@ module SolidQueueMonitor
|
|
|
6
6
|
PaginationService.new(relation, current_page, per_page).paginate
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
def render_page(title, content, search_query: nil)
|
|
10
|
-
# Get flash message from instance variable (set by set_flash_message) or session
|
|
11
|
-
message = @flash_message
|
|
12
|
-
message_type = @flash_type
|
|
13
|
-
|
|
14
|
-
# Try to get from session as fallback, but don't fail if session unavailable
|
|
15
|
-
begin
|
|
16
|
-
message ||= session[:flash_message]
|
|
17
|
-
message_type ||= session[:flash_type]
|
|
18
|
-
|
|
19
|
-
# Clear the flash message from session after using it
|
|
20
|
-
session.delete(:flash_message) if message
|
|
21
|
-
session.delete(:flash_type) if message_type
|
|
22
|
-
rescue StandardError
|
|
23
|
-
# Session not available (e.g., no session middleware in tests)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
html = SolidQueueMonitor::HtmlGenerator.new(
|
|
27
|
-
title: title,
|
|
28
|
-
content: content,
|
|
29
|
-
message: message,
|
|
30
|
-
message_type: message_type,
|
|
31
|
-
search_query: search_query,
|
|
32
|
-
nonce: content_security_policy_nonce
|
|
33
|
-
).generate
|
|
34
|
-
|
|
35
|
-
render html: html.html_safe
|
|
36
|
-
end
|
|
37
|
-
|
|
38
9
|
def current_page
|
|
39
10
|
(params[:page] || 1).to_i
|
|
40
11
|
end
|
|
@@ -8,13 +8,9 @@ module SolidQueueMonitor
|
|
|
8
8
|
base_query = SolidQueue::FailedExecution.includes(:job)
|
|
9
9
|
sorted_query = apply_execution_sorting(filter_failed_jobs(base_query), SORTABLE_COLUMNS, 'created_at', :desc)
|
|
10
10
|
@failed_jobs = paginate(sorted_query)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
total_pages: @failed_jobs[:total_pages],
|
|
15
|
-
filters: filter_params,
|
|
16
|
-
sort: sort_params,
|
|
17
|
-
nonce: content_security_policy_nonce).render)
|
|
11
|
+
@filters = filter_params
|
|
12
|
+
@sort = sort_params
|
|
13
|
+
@action_path = failed_jobs_path
|
|
18
14
|
end
|
|
19
15
|
|
|
20
16
|
def retry
|
|
@@ -8,12 +8,9 @@ module SolidQueueMonitor
|
|
|
8
8
|
base_query = SolidQueue::ClaimedExecution.includes(:job)
|
|
9
9
|
sorted_query = apply_execution_sorting(filter_in_progress_jobs(base_query), SORTABLE_COLUMNS, 'created_at', :desc)
|
|
10
10
|
@in_progress_jobs = paginate(sorted_query)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
total_pages: @in_progress_jobs[:total_pages],
|
|
15
|
-
filters: filter_params,
|
|
16
|
-
sort: sort_params).render)
|
|
11
|
+
@filters = filter_params
|
|
12
|
+
@sort = sort_params
|
|
13
|
+
@action_path = in_progress_jobs_path
|
|
17
14
|
end
|
|
18
15
|
|
|
19
16
|
private
|
|
@@ -11,13 +11,9 @@ module SolidQueueMonitor
|
|
|
11
11
|
return
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@job,
|
|
18
|
-
**job_data,
|
|
19
|
-
nonce: content_security_policy_nonce
|
|
20
|
-
).render)
|
|
14
|
+
load_job_data(@job).each do |name, value|
|
|
15
|
+
instance_variable_set("@#{name}", value)
|
|
16
|
+
end
|
|
21
17
|
end
|
|
22
18
|
|
|
23
19
|
private
|
|
@@ -7,14 +7,15 @@ module SolidQueueMonitor
|
|
|
7
7
|
def index
|
|
8
8
|
@stats = SolidQueueMonitor::StatsCalculator.calculate
|
|
9
9
|
@chart_data = SolidQueueMonitor.show_chart ? SolidQueueMonitor::ChartDataService.new(time_range: time_range_param).calculate : nil
|
|
10
|
+
@time_range = time_range_param
|
|
10
11
|
|
|
11
12
|
recent_jobs_query = SolidQueue::Job.limit(100)
|
|
12
13
|
sorted_query = apply_sorting(filter_jobs(recent_jobs_query), SORTABLE_COLUMNS, 'created_at', :desc)
|
|
13
14
|
@recent_jobs = paginate(sorted_query)
|
|
15
|
+
@filters = filter_params
|
|
16
|
+
@sort = sort_params
|
|
14
17
|
|
|
15
18
|
preload_job_statuses(@recent_jobs[:records])
|
|
16
|
-
|
|
17
|
-
render_page('Overview', generate_overview_content)
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def chart_data
|
|
@@ -27,15 +28,5 @@ module SolidQueueMonitor
|
|
|
27
28
|
def time_range_param
|
|
28
29
|
params[:time_range] || ChartDataService::DEFAULT_TIME_RANGE
|
|
29
30
|
end
|
|
30
|
-
|
|
31
|
-
def generate_overview_content
|
|
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
|
|
39
|
-
end
|
|
40
31
|
end
|
|
41
32
|
end
|
|
@@ -11,12 +11,7 @@ module SolidQueueMonitor
|
|
|
11
11
|
@queues = apply_queue_sorting(base_query)
|
|
12
12
|
@paused_queues = QueuePauseService.paused_queues
|
|
13
13
|
@queue_stats = aggregate_queue_stats
|
|
14
|
-
|
|
15
|
-
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(
|
|
16
|
-
@queues, @paused_queues,
|
|
17
|
-
queue_stats: @queue_stats,
|
|
18
|
-
sort: sort_params
|
|
19
|
-
).render)
|
|
14
|
+
@sort = sort_params
|
|
20
15
|
end
|
|
21
16
|
|
|
22
17
|
def show
|
|
@@ -30,18 +25,9 @@ module SolidQueueMonitor
|
|
|
30
25
|
preload_job_statuses(@jobs[:records])
|
|
31
26
|
|
|
32
27
|
@counts = calculate_queue_counts(@queue_name)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
queue_name: @queue_name,
|
|
37
|
-
paused: @paused,
|
|
38
|
-
jobs: @jobs[:records],
|
|
39
|
-
counts: @counts,
|
|
40
|
-
current_page: @jobs[:current_page],
|
|
41
|
-
total_pages: @jobs[:total_pages],
|
|
42
|
-
filters: queue_filter_params,
|
|
43
|
-
sort: sort_params
|
|
44
|
-
).render)
|
|
28
|
+
@filters = queue_filter_params
|
|
29
|
+
@sort = sort_params
|
|
30
|
+
@action_path = queue_details_path(queue_name: @queue_name)
|
|
45
31
|
end
|
|
46
32
|
|
|
47
33
|
def pause
|
|
@@ -8,12 +8,9 @@ module SolidQueueMonitor
|
|
|
8
8
|
base_query = SolidQueue::ReadyExecution.includes(:job)
|
|
9
9
|
sorted_query = apply_execution_sorting(filter_ready_jobs(base_query), SORTABLE_COLUMNS, 'created_at', :desc)
|
|
10
10
|
@ready_jobs = paginate(sorted_query)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
total_pages: @ready_jobs[:total_pages],
|
|
15
|
-
filters: filter_params,
|
|
16
|
-
sort: sort_params).render)
|
|
11
|
+
@filters = filter_params
|
|
12
|
+
@sort = sort_params
|
|
13
|
+
@action_path = ready_jobs_path
|
|
17
14
|
end
|
|
18
15
|
end
|
|
19
16
|
end
|
|
@@ -8,12 +8,9 @@ module SolidQueueMonitor
|
|
|
8
8
|
base_query = filter_recurring_jobs(SolidQueue::RecurringTask.all)
|
|
9
9
|
sorted_query = apply_sorting(base_query, SORTABLE_COLUMNS, 'key', :asc)
|
|
10
10
|
@recurring_jobs = paginate(sorted_query)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
total_pages: @recurring_jobs[:total_pages],
|
|
15
|
-
filters: filter_params,
|
|
16
|
-
sort: sort_params).render)
|
|
11
|
+
@filters = filter_params
|
|
12
|
+
@sort = sort_params
|
|
13
|
+
@action_path = recurring_jobs_path
|
|
17
14
|
end
|
|
18
15
|
end
|
|
19
16
|
end
|
|
@@ -8,13 +8,9 @@ module SolidQueueMonitor
|
|
|
8
8
|
base_query = SolidQueue::ScheduledExecution.includes(:job)
|
|
9
9
|
sorted_query = apply_execution_sorting(filter_scheduled_jobs(base_query), SORTABLE_COLUMNS, 'scheduled_at', :asc)
|
|
10
10
|
@scheduled_jobs = paginate(sorted_query)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
total_pages: @scheduled_jobs[:total_pages],
|
|
15
|
-
filters: filter_params,
|
|
16
|
-
sort: sort_params,
|
|
17
|
-
nonce: content_security_policy_nonce).render)
|
|
11
|
+
@filters = filter_params
|
|
12
|
+
@sort = sort_params
|
|
13
|
+
@action_path = scheduled_jobs_path
|
|
18
14
|
end
|
|
19
15
|
|
|
20
16
|
def create
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class SearchController < BaseController
|
|
5
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)
|
|
6
|
+
@query = params[:q]
|
|
7
|
+
@results = SearchService.new(@query).search
|
|
8
|
+
@total_count = @results.values.sum(&:size)
|
|
10
9
|
end
|
|
11
10
|
end
|
|
12
11
|
end
|
|
@@ -8,14 +8,11 @@ module SolidQueueMonitor
|
|
|
8
8
|
base_query = SolidQueue::Process.all
|
|
9
9
|
sorted_query = apply_sorting(filter_workers(base_query), SORTABLE_COLUMNS, 'last_heartbeat_at', :desc)
|
|
10
10
|
@processes = paginate(sorted_query)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
filters: worker_filter_params,
|
|
17
|
-
sort: sort_params
|
|
18
|
-
).render)
|
|
11
|
+
@process_records = @processes[:records].to_a
|
|
12
|
+
@filters = worker_filter_params
|
|
13
|
+
@sort = sort_params
|
|
14
|
+
@summary = worker_summary
|
|
15
|
+
preload_claimed_data
|
|
19
16
|
end
|
|
20
17
|
|
|
21
18
|
def remove
|
|
@@ -73,5 +70,24 @@ module SolidQueueMonitor
|
|
|
73
70
|
status: params[:status]
|
|
74
71
|
}
|
|
75
72
|
end
|
|
73
|
+
|
|
74
|
+
def worker_summary
|
|
75
|
+
all_processes = SolidQueue::Process.all.to_a
|
|
76
|
+
{
|
|
77
|
+
total: all_processes.count,
|
|
78
|
+
healthy: all_processes.count { |process| view_context.worker_status(process) == :healthy },
|
|
79
|
+
stale: all_processes.count { |process| view_context.worker_status(process) == :stale },
|
|
80
|
+
dead: all_processes.count { |process| view_context.worker_status(process) == :dead }
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def preload_claimed_data
|
|
85
|
+
process_ids = @process_records.map(&:id)
|
|
86
|
+
@claimed_counts = SolidQueue::ClaimedExecution.where(process_id: process_ids).group(:process_id).count
|
|
87
|
+
@claimed_jobs = SolidQueue::ClaimedExecution.includes(:job).where(process_id: process_ids).each_with_object({}) do |execution, hash|
|
|
88
|
+
hash[execution.process_id] ||= []
|
|
89
|
+
hash[execution.process_id] << execution.job
|
|
90
|
+
end
|
|
91
|
+
end
|
|
76
92
|
end
|
|
77
93
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueMonitor
|
|
4
|
+
module ApplicationHelper
|
|
5
|
+
def asset_url_for(file_name)
|
|
6
|
+
base = File.basename(file_name, '.*')
|
|
7
|
+
ext = File.extname(file_name)
|
|
8
|
+
hash = SolidQueueMonitor::AssetCache.fingerprint_for(file_name)
|
|
9
|
+
fingerprinted_file = "#{base}-#{hash}#{ext}"
|
|
10
|
+
|
|
11
|
+
if respond_to?(:solid_queue_monitor)
|
|
12
|
+
solid_queue_monitor.asset_path(file: fingerprinted_file)
|
|
13
|
+
else
|
|
14
|
+
SolidQueueMonitor::Engine.routes.url_helpers.asset_path(file: fingerprinted_file)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def format_datetime(datetime)
|
|
19
|
+
return '-' unless datetime
|
|
20
|
+
|
|
21
|
+
datetime.strftime('%Y-%m-%d %H:%M:%S')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def message_class(type)
|
|
25
|
+
type.to_s == 'success' ? 'message-success' : 'message-error'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def queue_link(queue_name, css_class: nil)
|
|
29
|
+
return '-' if queue_name.blank?
|
|
30
|
+
|
|
31
|
+
link_to(queue_name,
|
|
32
|
+
queue_details_url_for(queue_name),
|
|
33
|
+
class: class_names('queue-link', css_class))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def queue_details_url_for(queue_name)
|
|
39
|
+
if respond_to?(:queue_details_path)
|
|
40
|
+
queue_details_path(queue_name: queue_name)
|
|
41
|
+
else
|
|
42
|
+
SolidQueueMonitor::Engine.routes.url_helpers.queue_details_path(queue_name: queue_name)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueMonitor
|
|
4
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
|
+
module ChartHelper
|
|
6
|
+
CHART_WIDTH = 1200
|
|
7
|
+
CHART_HEIGHT = 280
|
|
8
|
+
PADDING = { top: 40, right: 30, bottom: 60, left: 60 }.freeze
|
|
9
|
+
COLORS = {
|
|
10
|
+
created: '#3b82f6',
|
|
11
|
+
completed: '#10b981',
|
|
12
|
+
failed: '#ef4444'
|
|
13
|
+
}.freeze
|
|
14
|
+
SERIES = %i[failed completed created].freeze
|
|
15
|
+
|
|
16
|
+
def render_chart(data:, time_range: nil)
|
|
17
|
+
context = chart_context(data, time_range)
|
|
18
|
+
|
|
19
|
+
safe_join(
|
|
20
|
+
[
|
|
21
|
+
chart_section(context),
|
|
22
|
+
chart_tooltip
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def chart_time_range_options
|
|
28
|
+
SolidQueueMonitor::ChartDataService::TIME_RANGES.map do |key, config|
|
|
29
|
+
[config[:label], key]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def chart_context(data, time_range)
|
|
36
|
+
{
|
|
37
|
+
data: data.merge(time_range: time_range || data[:time_range]),
|
|
38
|
+
plot_width: CHART_WIDTH - PADDING[:left] - PADDING[:right],
|
|
39
|
+
plot_height: CHART_HEIGHT - PADDING[:top] - PADDING[:bottom]
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def chart_section(context)
|
|
44
|
+
tag.div(id: 'chart-section', class: 'chart-section') do
|
|
45
|
+
safe_join(
|
|
46
|
+
[
|
|
47
|
+
chart_header(context),
|
|
48
|
+
tag.div(id: 'chart-collapsible', class: 'chart-collapsible') do
|
|
49
|
+
safe_join(
|
|
50
|
+
[
|
|
51
|
+
tag.div(chart_body(context), class: 'chart-container'),
|
|
52
|
+
chart_legend
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def chart_header(context)
|
|
62
|
+
tag.div(class: 'chart-header') do
|
|
63
|
+
safe_join(
|
|
64
|
+
[
|
|
65
|
+
tag.div(class: 'chart-header-left') do
|
|
66
|
+
safe_join(
|
|
67
|
+
[
|
|
68
|
+
chart_toggle_button,
|
|
69
|
+
tag.h3('Job Activity'),
|
|
70
|
+
chart_summary(context)
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
end,
|
|
74
|
+
chart_time_select(context)
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def chart_toggle_button
|
|
81
|
+
tag.button(class: 'chart-toggle-btn', id: 'chart-toggle-btn', title: 'Toggle chart') do
|
|
82
|
+
tag.svg(class: 'chart-toggle-icon',
|
|
83
|
+
id: 'chart-toggle-icon',
|
|
84
|
+
width: 16,
|
|
85
|
+
height: 16,
|
|
86
|
+
viewBox: '0 0 24 24',
|
|
87
|
+
fill: 'none',
|
|
88
|
+
stroke: 'currentColor',
|
|
89
|
+
stroke_width: 2) do
|
|
90
|
+
tag.polyline(points: '6 9 12 15 18 9')
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def chart_summary(context)
|
|
96
|
+
totals = context[:data][:totals] || { created: 0, completed: 0, failed: 0 }
|
|
97
|
+
|
|
98
|
+
tag.span(class: 'chart-summary') do
|
|
99
|
+
safe_join(
|
|
100
|
+
[
|
|
101
|
+
tag.span("#{totals[:created]} created", class: 'summary-item summary-created'),
|
|
102
|
+
tag.span('.', class: 'summary-separator'),
|
|
103
|
+
tag.span("#{totals[:completed]} completed", class: 'summary-item summary-completed'),
|
|
104
|
+
tag.span('.', class: 'summary-separator'),
|
|
105
|
+
tag.span("#{totals[:failed]} failed", class: 'summary-item summary-failed')
|
|
106
|
+
],
|
|
107
|
+
' '
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def chart_time_select(context)
|
|
113
|
+
options = context[:data][:available_ranges].map do |key, label|
|
|
114
|
+
tag.option(label, value: key, selected: key == context[:data][:time_range])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
tag.div(class: 'chart-time-select-wrapper') do
|
|
118
|
+
tag.select(safe_join(options), class: 'chart-time-select', id: 'chart-time-select')
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def chart_body(context)
|
|
123
|
+
return chart_empty_state if all_series_empty?(context)
|
|
124
|
+
|
|
125
|
+
max_value = [calculate_max_value(context), 10].max
|
|
126
|
+
|
|
127
|
+
tag.svg(viewBox: "0 0 #{CHART_WIDTH} #{CHART_HEIGHT}",
|
|
128
|
+
class: 'job-activity-chart',
|
|
129
|
+
preserveAspectRatio: 'xMidYMid meet') do
|
|
130
|
+
safe_join(
|
|
131
|
+
[
|
|
132
|
+
chart_grid_lines(context),
|
|
133
|
+
chart_axes,
|
|
134
|
+
chart_x_labels(context),
|
|
135
|
+
chart_y_labels(context, max_value),
|
|
136
|
+
*SERIES.map { |series| series_line(context, series, max_value) },
|
|
137
|
+
*SERIES.map { |series| series_points(context, series, max_value) }
|
|
138
|
+
].compact
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def chart_empty_state
|
|
144
|
+
tag.div(class: 'chart-empty') do
|
|
145
|
+
tag.span('No job activity in this time range')
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def chart_grid_lines(context)
|
|
150
|
+
safe_join(
|
|
151
|
+
5.times.map do |index|
|
|
152
|
+
y = PADDING[:top] + (context[:plot_height] * index / 4.0)
|
|
153
|
+
tag.line(x1: PADDING[:left],
|
|
154
|
+
y1: y,
|
|
155
|
+
x2: CHART_WIDTH - PADDING[:right],
|
|
156
|
+
y2: y,
|
|
157
|
+
class: 'grid-line')
|
|
158
|
+
end
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def chart_axes
|
|
163
|
+
safe_join(
|
|
164
|
+
[
|
|
165
|
+
tag.line(x1: PADDING[:left],
|
|
166
|
+
y1: PADDING[:top],
|
|
167
|
+
x2: PADDING[:left],
|
|
168
|
+
y2: CHART_HEIGHT - PADDING[:bottom],
|
|
169
|
+
class: 'axis-line'),
|
|
170
|
+
tag.line(x1: PADDING[:left],
|
|
171
|
+
y1: CHART_HEIGHT - PADDING[:bottom],
|
|
172
|
+
x2: CHART_WIDTH - PADDING[:right],
|
|
173
|
+
y2: CHART_HEIGHT - PADDING[:bottom],
|
|
174
|
+
class: 'axis-line')
|
|
175
|
+
]
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def chart_x_labels(context)
|
|
180
|
+
labels = context[:data][:labels]
|
|
181
|
+
return ''.html_safe if labels.blank?
|
|
182
|
+
|
|
183
|
+
step = labels.size > 12 ? (labels.size / 6.0).ceil : 1
|
|
184
|
+
safe_join(labels.each_with_index.filter_map do |label, index|
|
|
185
|
+
next unless (index % step).zero? || index == labels.size - 1
|
|
186
|
+
|
|
187
|
+
tag.text(label,
|
|
188
|
+
x: x_for_index(context, index, labels.size),
|
|
189
|
+
y: CHART_HEIGHT - PADDING[:bottom] + 20,
|
|
190
|
+
class: 'axis-label x-label')
|
|
191
|
+
end)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def chart_y_labels(context, max_value)
|
|
195
|
+
safe_join(
|
|
196
|
+
5.times.map do |index|
|
|
197
|
+
value = (max_value * (4 - index) / 4.0).round
|
|
198
|
+
y = PADDING[:top] + (context[:plot_height] * index / 4.0)
|
|
199
|
+
tag.text(value, x: PADDING[:left] - 10, y: y + 4, class: 'axis-label y-label')
|
|
200
|
+
end
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def series_line(context, series, max_value)
|
|
205
|
+
return if series_empty?(context, series)
|
|
206
|
+
|
|
207
|
+
points = calculate_points(context, series, max_value)
|
|
208
|
+
return if points.empty?
|
|
209
|
+
|
|
210
|
+
tag.polyline(points: points.map { |point| "#{point[:x]},#{point[:y]}" }.join(' '),
|
|
211
|
+
class: "chart-line chart-line-#{series}",
|
|
212
|
+
fill: 'none',
|
|
213
|
+
stroke: COLORS[series],
|
|
214
|
+
stroke_width: 2)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def series_points(context, series, max_value)
|
|
218
|
+
return if series_empty?(context, series)
|
|
219
|
+
|
|
220
|
+
values = context[:data][series]
|
|
221
|
+
safe_join(calculate_points(context, series, max_value).each_with_index.map do |point, index|
|
|
222
|
+
tag.circle(cx: point[:x],
|
|
223
|
+
cy: point[:y],
|
|
224
|
+
r: 4,
|
|
225
|
+
class: "data-point data-point-#{series}",
|
|
226
|
+
fill: COLORS[series],
|
|
227
|
+
data: { series: series, label: context[:data][:labels][index], value: values[index] })
|
|
228
|
+
end)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def chart_legend
|
|
232
|
+
tag.div(class: 'chart-legend') do
|
|
233
|
+
safe_join(%i[created completed failed].map do |series|
|
|
234
|
+
tag.span(class: 'legend-item') do
|
|
235
|
+
safe_join(
|
|
236
|
+
[
|
|
237
|
+
tag.span('', class: "legend-color legend-color-#{series}"),
|
|
238
|
+
series.to_s.capitalize
|
|
239
|
+
],
|
|
240
|
+
"\n"
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
end)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def chart_tooltip
|
|
248
|
+
tag.div(id: 'chart-tooltip', class: 'chart-tooltip') do
|
|
249
|
+
safe_join(
|
|
250
|
+
[
|
|
251
|
+
tag.div('', class: 'tooltip-label'),
|
|
252
|
+
tag.div('', class: 'tooltip-value')
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def all_series_empty?(context)
|
|
259
|
+
%i[created completed failed].all? { |series| series_empty?(context, series) }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def series_empty?(context, series)
|
|
263
|
+
context[:data][series].nil? || context[:data][series].all?(&:zero?)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def calculate_max_value(context)
|
|
267
|
+
max = (context[:data][:created] + context[:data][:completed] + context[:data][:failed]).max || 0
|
|
268
|
+
return 10 if max <= 10
|
|
269
|
+
|
|
270
|
+
magnitude = 10**Math.log10(max).floor
|
|
271
|
+
((max.to_f / magnitude).ceil * magnitude)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def calculate_points(context, series, max_value)
|
|
275
|
+
values = context[:data][series]
|
|
276
|
+
return [] if values.blank?
|
|
277
|
+
|
|
278
|
+
values.each_with_index.map do |value, index|
|
|
279
|
+
{
|
|
280
|
+
x: x_for_index(context, index, values.size).round(2),
|
|
281
|
+
y: (CHART_HEIGHT - PADDING[:bottom] - (context[:plot_height] * value / max_value.to_f)).round(2)
|
|
282
|
+
}
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def x_for_index(context, index, count)
|
|
287
|
+
return PADDING[:left] if count <= 1
|
|
288
|
+
|
|
289
|
+
PADDING[:left] + (context[:plot_width] * index / (count - 1).to_f)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
# rubocop:enable Metrics/ModuleLength
|
|
293
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueMonitor
|
|
4
|
+
module JobDetailsHelper
|
|
5
|
+
def detail_job_status(job:, failed_execution:, claimed_execution:, scheduled_execution:)
|
|
6
|
+
return :failed if failed_execution
|
|
7
|
+
return :in_progress if claimed_execution
|
|
8
|
+
return :scheduled if scheduled_execution || job.scheduled_at&.future?
|
|
9
|
+
return :completed if job.finished_at
|
|
10
|
+
|
|
11
|
+
:pending
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def detail_status_label(status = detail_job_status)
|
|
15
|
+
{
|
|
16
|
+
failed: 'Failed',
|
|
17
|
+
in_progress: 'In Progress',
|
|
18
|
+
scheduled: 'Scheduled',
|
|
19
|
+
completed: 'Completed',
|
|
20
|
+
pending: 'Pending'
|
|
21
|
+
}[status]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def detail_status_class(status = detail_job_status)
|
|
25
|
+
"status-#{status.to_s.tr('_', '-')}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def detail_duration(seconds)
|
|
29
|
+
return '-' unless seconds
|
|
30
|
+
return "#{(seconds * 1000).round}ms" if seconds < 1
|
|
31
|
+
return "#{seconds.round(1)}s" if seconds < 60
|
|
32
|
+
return "#{(seconds / 60).floor}m #{(seconds % 60).round}s" if seconds < 3600
|
|
33
|
+
|
|
34
|
+
"#{(seconds / 3600).floor}h #{((seconds % 3600) / 60).floor}m"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def detail_timing(job:, claimed_execution:, failed_execution:)
|
|
38
|
+
created_at = job.created_at
|
|
39
|
+
started_at = claimed_execution&.created_at
|
|
40
|
+
finished_at = job.finished_at
|
|
41
|
+
failed_at = failed_execution&.created_at
|
|
42
|
+
end_time = finished_at || failed_at
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
queue_wait: started_at && created_at ? started_at - created_at : nil,
|
|
46
|
+
execution: started_at && end_time ? end_time - started_at : nil,
|
|
47
|
+
total: created_at && end_time ? end_time - created_at : nil
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def pretty_arguments(args)
|
|
52
|
+
return '-' if args.blank?
|
|
53
|
+
|
|
54
|
+
JSON.pretty_generate(args)
|
|
55
|
+
rescue JSON::GeneratorError
|
|
56
|
+
args.inspect
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def recent_job_duration(job)
|
|
60
|
+
end_time = job.finished_at || job.failed_execution&.created_at
|
|
61
|
+
return '-' unless end_time
|
|
62
|
+
|
|
63
|
+
detail_duration(end_time - job.created_at)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|