rails_pulse 0.3.3 → 0.4.0.pre.1
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/CHANGELOG.md +272 -0
- data/README.md +44 -3
- data/app/assets/stylesheets/rails_pulse/application.css +21 -32
- data/app/assets/stylesheets/rails_pulse/components/backtrace.css +238 -0
- data/app/assets/stylesheets/rails_pulse/components/badge.css +32 -0
- data/app/assets/stylesheets/rails_pulse/components/button.css +2 -36
- data/app/assets/stylesheets/rails_pulse/components/card.css +8 -11
- data/app/assets/stylesheets/rails_pulse/components/chart.css +16 -3
- data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +5 -39
- data/app/assets/stylesheets/rails_pulse/components/dashboard.css +0 -52
- data/app/assets/stylesheets/rails_pulse/components/deployments.css +39 -0
- data/app/assets/stylesheets/rails_pulse/components/dialog.css +0 -5
- data/app/assets/stylesheets/rails_pulse/components/input.css +0 -14
- data/app/assets/stylesheets/rails_pulse/components/layouts.css +0 -20
- data/app/assets/stylesheets/rails_pulse/components/menu.css +0 -16
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +0 -16
- data/app/assets/stylesheets/rails_pulse/components/storage.css +187 -0
- data/app/assets/stylesheets/rails_pulse/components/tags.css +0 -14
- data/app/assets/stylesheets/rails_pulse/components/time_range_selector.css +5 -5
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +0 -17
- data/app/controllers/concerns/chart_table_concern.rb +15 -3
- data/app/controllers/concerns/ransack_params_concern.rb +18 -0
- data/app/controllers/concerns/response_range_concern.rb +1 -1
- data/app/controllers/concerns/time_range_concern.rb +28 -14
- data/app/controllers/rails_pulse/application_controller.rb +104 -31
- data/app/controllers/rails_pulse/dashboard_controller.rb +9 -0
- data/app/controllers/rails_pulse/deployments_controller.rb +26 -5
- data/app/controllers/rails_pulse/exception_occurrences_controller.rb +19 -0
- data/app/controllers/rails_pulse/exceptions_controller.rb +119 -0
- data/app/controllers/rails_pulse/job_runs_controller.rb +1 -1
- data/app/controllers/rails_pulse/requests_controller.rb +1 -1
- data/app/controllers/rails_pulse/routes_controller.rb +7 -1
- data/app/controllers/rails_pulse/storage_controller.rb +7 -0
- data/app/controllers/rails_pulse/tags_controller.rb +2 -2
- data/app/helpers/rails_pulse/application_helper.rb +1 -0
- data/app/helpers/rails_pulse/backtrace_helper.rb +85 -0
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +4 -1
- data/app/helpers/rails_pulse/chart_helper.rb +0 -3
- data/app/helpers/rails_pulse/route_helper.rb +8 -16
- data/app/javascript/rails_pulse/application.js +29 -8
- data/app/javascript/rails_pulse/controllers/clipboard_controller.js +58 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +16 -10
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +6 -2
- data/app/javascript/rails_pulse/controllers/popover_controller.js +4 -0
- data/app/models/concerns/rails_pulse/taggable.rb +11 -6
- data/app/models/rails_pulse/cards/base.rb +53 -0
- data/app/models/rails_pulse/dashboard/health_summary.rb +52 -0
- data/app/models/rails_pulse/dashboard/needs_attention.rb +73 -1
- data/app/models/rails_pulse/dashboard/storage_status.rb +490 -0
- data/app/models/rails_pulse/deployment.rb +47 -1
- data/app/models/rails_pulse/exception_group.rb +37 -0
- data/app/models/rails_pulse/exception_occurrence.rb +27 -0
- data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +116 -0
- data/app/models/rails_pulse/exceptions/cards/open_groups.rb +49 -0
- data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +62 -0
- data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +56 -0
- data/app/models/rails_pulse/exceptions/raw_data.rb +216 -0
- data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -2
- data/app/models/rails_pulse/operation.rb +13 -4
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +8 -3
- data/app/models/rails_pulse/request.rb +5 -1
- data/app/models/rails_pulse/route.rb +71 -21
- data/app/models/rails_pulse/routes/cards/error_rates.rb +6 -3
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +8 -3
- data/app/models/rails_pulse/routes/tables/index.rb +8 -6
- data/app/models/rails_pulse/summary.rb +21 -10
- data/app/models/rails_pulse/tables/base.rb +4 -2
- data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +47 -12
- data/app/services/rails_pulse/exception_capture_service.rb +228 -0
- data/app/services/rails_pulse/exception_message_sanitizer.rb +152 -0
- data/app/services/rails_pulse/operations/change_point.rb +144 -0
- data/app/services/rails_pulse/operations/compare.rb +92 -0
- data/app/services/rails_pulse/operations/comparison.rb +126 -0
- data/app/services/rails_pulse/operations/metric.rb +63 -0
- data/app/services/rails_pulse/operations/series.rb +84 -0
- data/app/services/rails_pulse/operations/subject.rb +77 -0
- data/app/services/rails_pulse/route_controller_action_backfiller.rb +110 -0
- data/app/services/rails_pulse/route_merger.rb +99 -0
- data/app/services/rails_pulse/route_migrator.rb +139 -0
- data/app/services/rails_pulse/route_path_normalizer.rb +79 -0
- data/app/services/rails_pulse/route_recognizer.rb +47 -0
- data/app/services/rails_pulse/summary_service.rb +45 -0
- data/app/services/rails_pulse/tag_filter_service.rb +5 -3
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
- data/app/views/layouts/rails_pulse/_onboarding_banner.html.erb +8 -0
- data/app/views/layouts/rails_pulse/_setup_screen.html.erb +5 -5
- data/app/views/layouts/rails_pulse/application.html.erb +4 -4
- data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
- data/app/views/rails_pulse/components/_panel.html.erb +16 -6
- data/app/views/rails_pulse/components/_raw_data.html.erb +34 -0
- data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -8
- data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +64 -0
- data/app/views/rails_pulse/dashboard/_health_badge.html.erb +21 -10
- data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +67 -0
- data/app/views/rails_pulse/dashboard/index.html.erb +22 -1
- data/app/views/rails_pulse/deployments/_status_badge.html.erb +5 -0
- data/app/views/rails_pulse/deployments/_table.html.erb +29 -0
- data/app/views/rails_pulse/deployments/index.html.erb +15 -0
- data/app/views/rails_pulse/deployments/show.html.erb +62 -0
- data/app/views/rails_pulse/exception_occurrences/show.html.erb +45 -0
- data/app/views/rails_pulse/exceptions/_backtrace.html.erb +33 -0
- data/app/views/rails_pulse/exceptions/_metric_cards.html.erb +6 -0
- data/app/views/rails_pulse/exceptions/_occurrences_table.html.erb +23 -0
- data/app/views/rails_pulse/exceptions/_status_badge.html.erb +6 -0
- data/app/views/rails_pulse/exceptions/_table.html.erb +33 -0
- data/app/views/rails_pulse/exceptions/index.html.erb +66 -0
- data/app/views/rails_pulse/exceptions/show.html.erb +52 -0
- data/app/views/rails_pulse/queries/_raw_data.html.erb +1 -23
- data/app/views/rails_pulse/requests/_table.html.erb +1 -1
- data/app/views/rails_pulse/routes/_requests_table.html.erb +5 -0
- data/app/views/rails_pulse/routes/_table.html.erb +22 -8
- data/app/views/rails_pulse/routes/index.html.erb +5 -1
- data/app/views/rails_pulse/routes/show.html.erb +5 -0
- data/app/views/rails_pulse/storage/show.html.erb +123 -0
- data/config/routes.rb +13 -4
- data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +50 -0
- data/db/rails_pulse_migrate/20260507000001_add_actual_query_to_operations.rb +10 -5
- data/db/rails_pulse_migrate/20260610000001_add_controller_action_to_rails_pulse_routes.rb +8 -0
- data/db/rails_pulse_migrate/20260610000002_change_rails_pulse_routes_to_multi_verb_model.rb +185 -0
- data/db/rails_pulse_migrate/20260610000003_add_null_action_unique_index_to_routes.rb +32 -0
- data/db/rails_pulse_migrate/20260823000001_add_location_to_exception_groups.rb +12 -0
- data/db/rails_pulse_schema.rb +54 -6
- data/lib/generators/rails_pulse/base_methods.rb +2 -0
- data/lib/generators/rails_pulse/install_generator.rb +5 -0
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +54 -6
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +152 -16
- data/lib/generators/rails_pulse/upgrade_generator.rb +166 -38
- data/lib/rails_pulse/cleanup_service.rb +110 -15
- data/lib/rails_pulse/configuration.rb +125 -6
- data/lib/rails_pulse/engine.rb +32 -17
- data/lib/rails_pulse/installers/config_updater.rb +202 -0
- data/lib/rails_pulse/job_run_collector.rb +43 -16
- data/lib/rails_pulse/middleware/asset_server.rb +11 -0
- data/lib/rails_pulse/middleware/request_collector.rb +9 -4
- data/lib/rails_pulse/packaged_assets.rb +147 -0
- data/lib/rails_pulse/route_indexes.rb +66 -0
- data/lib/rails_pulse/subscribers/exception_subscriber.rb +36 -0
- data/lib/rails_pulse/tracker.rb +12 -9
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +1 -0
- data/lib/rails_pulse_server.ru +16 -47
- data/lib/tasks/rails_pulse.rake +83 -4
- data/lib/tasks/rails_pulse_assets.rake +15 -0
- data/lib/tasks/rails_pulse_tasks.rake +29 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +6 -2
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +18 -117
- metadata +78 -37
- data/Rakefile +0 -554
- data/app/javascript/rails_pulse/controllers/form_controller.js +0 -39
- data/app/javascript/rails_pulse/controllers/period_selector_controller.js +0 -19
- data/app/javascript/rails_pulse/controllers/timezone_controller.js +0 -89
- data/app/javascript/rails_pulse/theme.js +0 -416
- data/config/brakeman.ignore +0 -190
- data/config/brakeman.yml +0 -68
- data/config/initializers/rails_pulse.rb +0 -287
- data/public/rails-pulse-assets/csp-test.js +0 -120
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +0 -13
- data/public/rails-pulse-assets/rails-pulse.css.map +0 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +0 -7
- data/vendor/assets/javascripts/rails-pulse-icons.js +0 -93
- data/vendor/assets/javascripts/rails-pulse-icons.js.map +0 -13
- data/vendor/assets/javascripts/rails-pulse.js +0 -157
- data/vendor/assets/javascripts/rails-pulse.js.map +0 -7
- data/vendor/assets/stylesheets/rails-pulse.css +0 -1
- data/vendor/assets/stylesheets/rails-pulse.css.map +0 -1
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
.time-range-trigger:hover {
|
|
20
20
|
background: var(--color-secondary);
|
|
21
|
-
border-color: var(--color-border-
|
|
21
|
+
border-color: var(--color-border-dark);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
.time-range-trigger svg {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
.time-range-trigger:has(+ [popover]:popover-open) {
|
|
30
30
|
background: var(--color-secondary);
|
|
31
|
-
border-color: var(--color-border-
|
|
31
|
+
border-color: var(--color-border-dark);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
.time-range-trigger:has(+ [popover]:popover-open) svg {
|
|
@@ -36,16 +36,16 @@
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/* Light mode improvements */
|
|
39
|
-
html[data-color-scheme="
|
|
39
|
+
html:not([data-color-scheme="dark"]) .time-range-trigger {
|
|
40
40
|
border-color: rgba(0, 0, 0, 0.2);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
html[data-color-scheme="
|
|
43
|
+
html:not([data-color-scheme="dark"]) .time-range-trigger:hover {
|
|
44
44
|
background: rgba(0, 0, 0, 0.05);
|
|
45
45
|
border-color: rgba(0, 0, 0, 0.3);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
html[data-color-scheme="
|
|
48
|
+
html:not([data-color-scheme="dark"]) .time-range-trigger:has(+ [popover]:popover-open) {
|
|
49
49
|
background: rgba(0, 0, 0, 0.05);
|
|
50
50
|
border-color: rgba(0, 0, 0, 0.3);
|
|
51
51
|
}
|
|
@@ -3,37 +3,20 @@
|
|
|
3
3
|
.w-4 { width: 1rem; }
|
|
4
4
|
.w-6 { width: 1.5rem; }
|
|
5
5
|
.w-8 { width: 2rem; }
|
|
6
|
-
.w-12 { width: 3rem; }
|
|
7
6
|
.w-16 { width: 4rem; }
|
|
8
7
|
.w-20 { width: 5rem; }
|
|
9
8
|
.w-24 { width: 6rem; }
|
|
10
9
|
.w-28 { width: 7rem; }
|
|
11
10
|
.w-32 { width: 8rem; }
|
|
12
11
|
.w-36 { width: 9rem; }
|
|
13
|
-
.w-40 { width: 10rem; }
|
|
14
|
-
.w-44 { width: 11rem; }
|
|
15
12
|
.w-48 { width: 12rem; }
|
|
16
|
-
.w-52 { width: 13rem; }
|
|
17
13
|
.w-56 { width: 14rem; }
|
|
18
14
|
.w-60 { width: 15rem; }
|
|
19
|
-
.w-64 { width: 16rem; }
|
|
20
15
|
|
|
21
16
|
/* Min-width utilities */
|
|
22
|
-
.min-w-0 { min-width: 0; }
|
|
23
|
-
.min-w-4 { min-width: 1rem; }
|
|
24
|
-
.min-w-8 { min-width: 2rem; }
|
|
25
|
-
.min-w-12 { min-width: 3rem; }
|
|
26
|
-
.min-w-16 { min-width: 4rem; }
|
|
27
|
-
.min-w-20 { min-width: 5rem; }
|
|
28
|
-
.min-w-24 { min-width: 6rem; }
|
|
29
|
-
.min-w-32 { min-width: 8rem; }
|
|
30
17
|
|
|
31
18
|
/* Max-width utilities */
|
|
32
|
-
.max-w-xs { max-width: 20rem; }
|
|
33
|
-
.max-w-sm { max-width: 24rem; }
|
|
34
|
-
.max-w-md { max-width: 28rem; }
|
|
35
19
|
.max-w-lg { max-width: 32rem; }
|
|
36
|
-
.max-w-xl { max-width: 36rem; }
|
|
37
20
|
|
|
38
21
|
/* Table layout utilities */
|
|
39
22
|
.table-fixed { table-layout: fixed; }
|
|
@@ -53,7 +53,7 @@ module ChartTableConcern
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def setup_chart_and_table_data
|
|
56
|
-
ransack_params =
|
|
56
|
+
ransack_params = self.ransack_params
|
|
57
57
|
|
|
58
58
|
unless partial_request?
|
|
59
59
|
# Setup chart data first using original time range (no sorting from table)
|
|
@@ -172,11 +172,18 @@ module ChartTableConcern
|
|
|
172
172
|
action_name == "show"
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
+
# Hook: override to return param keys that should be excluded from chart queries.
|
|
176
|
+
# Use this when a filter requires a JOIN that chart queries don't perform
|
|
177
|
+
# (e.g. route path/action filters that need rails_pulse_routes joined).
|
|
178
|
+
def chart_filter_exclusions
|
|
179
|
+
[]
|
|
180
|
+
end
|
|
181
|
+
|
|
175
182
|
# Builds ransack parameters for chart queries
|
|
176
183
|
# Common pattern: time range + optional duration filter + resource scope
|
|
177
184
|
# Handles "recent" mode where @page_timings.start_time/@end_time may be nil
|
|
178
185
|
def build_chart_ransack_params(ransack_params)
|
|
179
|
-
base_params = ransack_params.except(:s)
|
|
186
|
+
base_params = ransack_params.except(:s, *chart_filter_exclusions)
|
|
180
187
|
|
|
181
188
|
# Add time filters if we have time boundaries (not in "recent" mode)
|
|
182
189
|
if @page_timings&.start_time && @page_timings&.end_time
|
|
@@ -196,7 +203,7 @@ module ChartTableConcern
|
|
|
196
203
|
|
|
197
204
|
# Scope to specific resource on show pages
|
|
198
205
|
if show_action?
|
|
199
|
-
base_params.merge(
|
|
206
|
+
base_params.merge(resource_id_scope)
|
|
200
207
|
else
|
|
201
208
|
base_params
|
|
202
209
|
end
|
|
@@ -308,4 +315,9 @@ module ChartTableConcern
|
|
|
308
315
|
def current_resource
|
|
309
316
|
nil
|
|
310
317
|
end
|
|
318
|
+
|
|
319
|
+
# Ransack scope for chart summaries on show pages. Override to customize (e.g. IN queries).
|
|
320
|
+
def resource_id_scope
|
|
321
|
+
{ summarizable_id_eq: current_resource.id }
|
|
322
|
+
end
|
|
311
323
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# RansackParamsConcern
|
|
2
|
+
#
|
|
3
|
+
# Every filterable page reads its Ransack conditions from params[:q]. The
|
|
4
|
+
# dashboard's own forms always send a hash, but a hand-edited URL can make it
|
|
5
|
+
# a string or an array (`?q=foo`), and each caller would then invoke Hash
|
|
6
|
+
# methods on it and 500. Normalise once, here.
|
|
7
|
+
module RansackParamsConcern
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# params[:q] as an ActionController::Parameters hash, or an empty one when
|
|
13
|
+
# the value is absent or not hash-shaped.
|
|
14
|
+
def ransack_params
|
|
15
|
+
q = params[:q]
|
|
16
|
+
q.respond_to?(:permit) ? q : ActionController::Parameters.new({})
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
# Supports both page-specific duration filters and global performance threshold filters.
|
|
6
6
|
module ResponseRangeConcern
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
|
+
include RansackParamsConcern
|
|
8
9
|
|
|
9
10
|
def setup_duration_range(type = :route)
|
|
10
|
-
ransack_params = params[:q] || {}
|
|
11
11
|
thresholds = RailsPulse.configuration.public_send("#{type}_thresholds")
|
|
12
12
|
|
|
13
13
|
# Check all duration-related parameters
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# Normalizes times to beginning/end of hour or day based on range duration.
|
|
12
12
|
module TimeRangeConcern
|
|
13
13
|
extend ActiveSupport::Concern
|
|
14
|
+
include RansackParamsConcern
|
|
14
15
|
|
|
15
16
|
included do
|
|
16
17
|
# Define the constant in the including class - ordered by most common usage
|
|
@@ -40,8 +41,6 @@ module TimeRangeConcern
|
|
|
40
41
|
end_time = Time.zone.now
|
|
41
42
|
selected_time_range = default_key
|
|
42
43
|
|
|
43
|
-
ransack_params = params[:q] || {}
|
|
44
|
-
|
|
45
44
|
# Priority 1: Page-specific preset from dropdown (check this first!)
|
|
46
45
|
if ransack_params[:period_start_range].present? && ransack_params[:period_start_range].to_sym != :custom
|
|
47
46
|
# Predefined time range from dropdown
|
|
@@ -59,22 +58,30 @@ module TimeRangeConcern
|
|
|
59
58
|
elsif ransack_params[:period_start_range].present? && ransack_params[:period_start_range].to_sym == :custom && ransack_params[:custom_date_range].present? && ransack_params[:custom_date_range].include?(" to ")
|
|
60
59
|
# Custom datetime range from custom range picker
|
|
61
60
|
dates = ransack_params[:custom_date_range].split(" to ")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
custom_start = parse_time_param(dates[0].strip)
|
|
62
|
+
custom_end = parse_time_param(dates[1].strip)
|
|
63
|
+
if custom_start && custom_end
|
|
64
|
+
start_time = custom_start
|
|
65
|
+
end_time = custom_end
|
|
66
|
+
selected_time_range = :custom
|
|
67
|
+
end
|
|
65
68
|
# Priority 3: Page-specific filters (chart zoom)
|
|
66
69
|
elsif ransack_params[:occurred_at_gteq].present? && ransack_params[:occurred_at_lt].present?
|
|
67
70
|
# Custom time range from chart zoom
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
zoom_start = parse_time_param(ransack_params[:occurred_at_gteq])
|
|
72
|
+
zoom_end = parse_time_param(ransack_params[:occurred_at_lt])
|
|
73
|
+
if zoom_start && zoom_end
|
|
74
|
+
start_time = zoom_start
|
|
75
|
+
end_time = zoom_end
|
|
76
|
+
selected_time_range = :custom
|
|
77
|
+
end
|
|
71
78
|
# Priority 4: Time range selector (from session)
|
|
72
79
|
elsif session[:time_range_preference].present?
|
|
73
80
|
preference = session[:time_range_preference]
|
|
74
81
|
if preference.is_a?(Hash) && preference["type"] == "custom"
|
|
75
82
|
# Custom range from time range selector
|
|
76
|
-
start_time = parse_time_param(preference["start_time"])
|
|
77
|
-
end_time = parse_time_param(preference["end_time"])
|
|
83
|
+
start_time = parse_time_param(preference["start_time"]) || start_time
|
|
84
|
+
end_time = parse_time_param(preference["end_time"]) || end_time
|
|
78
85
|
selected_time_range = :custom
|
|
79
86
|
else
|
|
80
87
|
# Preset from time range selector
|
|
@@ -90,8 +97,8 @@ module TimeRangeConcern
|
|
|
90
97
|
end
|
|
91
98
|
# Priority 5: Global filters (from session)
|
|
92
99
|
elsif session_global_filters["start_time"].present? || session_global_filters["end_time"].present?
|
|
93
|
-
start_time = parse_time_param(session_global_filters["start_time"])
|
|
94
|
-
end_time = parse_time_param(session_global_filters["end_time"])
|
|
100
|
+
start_time = parse_time_param(session_global_filters["start_time"]) || start_time
|
|
101
|
+
end_time = parse_time_param(session_global_filters["end_time"]) || end_time
|
|
95
102
|
selected_time_range = :custom
|
|
96
103
|
end
|
|
97
104
|
# Priority 6: Default time range (already set above)
|
|
@@ -112,17 +119,24 @@ module TimeRangeConcern
|
|
|
112
119
|
|
|
113
120
|
private
|
|
114
121
|
|
|
122
|
+
# Returns nil for anything that does not parse, so a hand-edited or stale
|
|
123
|
+
# value falls back to the default range instead of raising.
|
|
115
124
|
def parse_time_param(param)
|
|
116
125
|
case param
|
|
117
126
|
when Time, DateTime
|
|
118
127
|
param.in_time_zone
|
|
119
128
|
when String
|
|
129
|
+
return nil if param.blank?
|
|
130
|
+
|
|
120
131
|
# Parse as server local time (not UTC, not Time.zone)
|
|
121
132
|
# This ensures flatpickr datetime strings are interpreted in server's timezone
|
|
122
133
|
Time.parse(param).localtime
|
|
134
|
+
when Numeric
|
|
135
|
+
Time.zone.at(param)
|
|
123
136
|
else
|
|
124
|
-
|
|
125
|
-
Time.zone.at(param.to_i)
|
|
137
|
+
nil
|
|
126
138
|
end
|
|
139
|
+
rescue ArgumentError, TypeError, RangeError
|
|
140
|
+
nil
|
|
127
141
|
end
|
|
128
142
|
end
|
|
@@ -2,11 +2,30 @@ module RailsPulse
|
|
|
2
2
|
class ApplicationController < ActionController::Base
|
|
3
3
|
include PaginationConcern
|
|
4
4
|
include SessionFiltersConcern
|
|
5
|
+
include RansackParamsConcern
|
|
6
|
+
|
|
7
|
+
# Declared here rather than inherited. ActionController::Base only gets
|
|
8
|
+
# protect_from_forgery from the host's `default_protect_from_forgery`,
|
|
9
|
+
# which load_defaults 5.2+ turns on — a host still on an older
|
|
10
|
+
# load_defaults (or one that set the flag false and calls
|
|
11
|
+
# protect_from_forgery in its own ApplicationController) would leave every
|
|
12
|
+
# state-changing engine endpoint forgeable. Idempotent when the host
|
|
13
|
+
# already has it; the deployments API and assets controller keep their
|
|
14
|
+
# explicit skips.
|
|
15
|
+
protect_from_forgery with: :exception
|
|
5
16
|
|
|
6
17
|
before_action :authenticate_rails_pulse_user!
|
|
7
18
|
before_action :set_onboarding_state
|
|
8
19
|
before_action :load_deployment_markers
|
|
9
20
|
|
|
21
|
+
# Values accepted into the cookie session. Everything else is dropped:
|
|
22
|
+
# the session has a 4 KB ceiling and a stored value is re-parsed on every
|
|
23
|
+
# later request, so an unbounded or unparseable string would 500 the
|
|
24
|
+
# dashboard until the cookie is cleared.
|
|
25
|
+
TIME_RANGE_PRESETS = %w[last_24_hours last_7_days last_14_days last_30_days].freeze
|
|
26
|
+
PERFORMANCE_THRESHOLDS = %w[slow very_slow critical].freeze
|
|
27
|
+
MAX_TIME_PARAM_LENGTH = 64
|
|
28
|
+
|
|
10
29
|
def set_global_filters
|
|
11
30
|
if params[:clear] == "true"
|
|
12
31
|
session.delete(:global_filters)
|
|
@@ -15,21 +34,24 @@ module RailsPulse
|
|
|
15
34
|
filters = session[:global_filters] || {}
|
|
16
35
|
|
|
17
36
|
# Update time filters if provided
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
start_time = valid_time_param(params[:start_time])
|
|
38
|
+
end_time = valid_time_param(params[:end_time])
|
|
39
|
+
if start_time && end_time
|
|
40
|
+
filters["start_time"] = start_time
|
|
41
|
+
filters["end_time"] = end_time
|
|
21
42
|
end
|
|
22
43
|
|
|
23
|
-
# Update performance threshold if provided (or remove if empty)
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
# Update performance threshold if provided (or remove if empty/unknown)
|
|
45
|
+
threshold = params[:performance_threshold].to_s
|
|
46
|
+
if PERFORMANCE_THRESHOLDS.include?(threshold)
|
|
47
|
+
filters["performance_threshold"] = threshold
|
|
26
48
|
else
|
|
27
49
|
filters.delete("performance_threshold")
|
|
28
50
|
end
|
|
29
51
|
|
|
30
52
|
# Update tag visibility - convert enabled tags to disabled tags
|
|
31
53
|
all_tags = RailsPulse.configuration.tags
|
|
32
|
-
enabled_tags = params[:enabled_tags]
|
|
54
|
+
enabled_tags = Array(params[:enabled_tags]).map(&:to_s)
|
|
33
55
|
|
|
34
56
|
# Handle "non_tagged" separately
|
|
35
57
|
session[:show_non_tagged] = enabled_tags.include?("non_tagged")
|
|
@@ -50,28 +72,45 @@ module RailsPulse
|
|
|
50
72
|
end
|
|
51
73
|
|
|
52
74
|
# Redirect back to the referring page or root
|
|
53
|
-
redirect_back(fallback_location: root_path)
|
|
75
|
+
redirect_back(fallback_location: root_path, allow_other_host: false)
|
|
54
76
|
end
|
|
55
77
|
|
|
56
78
|
def set_time_range
|
|
57
|
-
|
|
79
|
+
preset = params[:preset].to_s
|
|
80
|
+
start_time = valid_time_param(params[:start_time])
|
|
81
|
+
end_time = valid_time_param(params[:end_time])
|
|
82
|
+
|
|
83
|
+
if preset.present?
|
|
58
84
|
# Store preset selection
|
|
59
|
-
session[:time_range_preference] =
|
|
60
|
-
elsif
|
|
85
|
+
session[:time_range_preference] = preset if TIME_RANGE_PRESETS.include?(preset)
|
|
86
|
+
elsif start_time && end_time
|
|
61
87
|
# Store custom range
|
|
62
88
|
session[:time_range_preference] = {
|
|
63
89
|
type: "custom",
|
|
64
|
-
start_time:
|
|
65
|
-
end_time:
|
|
90
|
+
start_time: start_time,
|
|
91
|
+
end_time: end_time
|
|
66
92
|
}
|
|
67
93
|
end
|
|
68
94
|
|
|
69
95
|
# Redirect back to the referring page or root
|
|
70
|
-
redirect_back(fallback_location: root_path)
|
|
96
|
+
redirect_back(fallback_location: root_path, allow_other_host: false)
|
|
71
97
|
end
|
|
72
98
|
|
|
73
99
|
private
|
|
74
100
|
|
|
101
|
+
# The original string if it is short enough to live in the session and
|
|
102
|
+
# Time.parse accepts it (TimeRangeConcern parses it back the same way);
|
|
103
|
+
# nil otherwise.
|
|
104
|
+
def valid_time_param(value)
|
|
105
|
+
value = value.to_s.strip
|
|
106
|
+
return nil if value.blank? || value.length > MAX_TIME_PARAM_LENGTH
|
|
107
|
+
|
|
108
|
+
Time.parse(value)
|
|
109
|
+
value
|
|
110
|
+
rescue ArgumentError, TypeError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
75
114
|
def partial_request?
|
|
76
115
|
request.headers["X-Partial-Request"] == "true"
|
|
77
116
|
end
|
|
@@ -80,33 +119,65 @@ module RailsPulse
|
|
|
80
119
|
RailsPulse.logger
|
|
81
120
|
end
|
|
82
121
|
|
|
122
|
+
# Two hooks, run in order:
|
|
123
|
+
#
|
|
124
|
+
# * `authentication_method` — the legacy hook. It denies by rendering or
|
|
125
|
+
# redirecting (the documented `unless signed_in? then redirect_to`
|
|
126
|
+
# style). Returning literal `false` without responding is also a
|
|
127
|
+
# denial; returning nil (which `unless … end` does on success) allows.
|
|
128
|
+
# * `authorize` — a fail-closed predicate. Anything falsy is a 403.
|
|
129
|
+
#
|
|
130
|
+
# With neither configured, HTTP Basic against RAILS_PULSE_PASSWORD.
|
|
83
131
|
def authenticate_rails_pulse_user!
|
|
84
|
-
|
|
132
|
+
config = RailsPulse.configuration
|
|
133
|
+
return unless config.authentication_enabled
|
|
85
134
|
|
|
86
|
-
|
|
87
|
-
if RailsPulse.configuration.authentication_method.nil?
|
|
135
|
+
if config.authentication_method.nil? && config.authorize.nil?
|
|
88
136
|
return fallback_http_basic_auth
|
|
89
137
|
end
|
|
90
138
|
|
|
91
|
-
|
|
92
|
-
|
|
139
|
+
unless config.authentication_method.nil?
|
|
140
|
+
run_authentication_method(config.authentication_method)
|
|
141
|
+
return if performed?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
run_authorize_predicate(config.authorize) if config.authorize
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
logger.warn "RailsPulse authentication failed: #{e.message}"
|
|
147
|
+
redirect_to config.authentication_redirect_path
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def run_authentication_method(hook)
|
|
151
|
+
result = case hook
|
|
93
152
|
when Proc
|
|
94
|
-
instance_exec(&
|
|
153
|
+
instance_exec(&hook)
|
|
95
154
|
when Symbol, String
|
|
96
|
-
method_name =
|
|
97
|
-
|
|
98
|
-
send(method_name)
|
|
99
|
-
else
|
|
155
|
+
method_name = hook.to_s
|
|
156
|
+
unless respond_to?(method_name, true)
|
|
100
157
|
logger.error "RailsPulse: Authentication method '#{method_name}' not found"
|
|
101
|
-
render plain: "Authentication configuration error", status: :internal_server_error
|
|
158
|
+
return render plain: "Authentication configuration error", status: :internal_server_error
|
|
102
159
|
end
|
|
160
|
+
send(method_name)
|
|
103
161
|
else
|
|
104
|
-
logger.error "RailsPulse: Invalid authentication method type: #{
|
|
105
|
-
render plain: "Authentication configuration error", status: :internal_server_error
|
|
162
|
+
logger.error "RailsPulse: Invalid authentication method type: #{hook.class}"
|
|
163
|
+
return render plain: "Authentication configuration error", status: :internal_server_error
|
|
106
164
|
end
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
165
|
+
|
|
166
|
+
return if performed?
|
|
167
|
+
return unless result == false
|
|
168
|
+
|
|
169
|
+
# `proc { current_user&.admin? }` reads as a predicate but never halts
|
|
170
|
+
# the chain. Treat an explicit false as the denial it was meant to be.
|
|
171
|
+
logger.warn "RailsPulse: authentication_method returned false without rendering or redirecting; " \
|
|
172
|
+
"denying access. Use config.authorize for predicate-style checks."
|
|
173
|
+
render plain: "Forbidden", status: :forbidden
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def run_authorize_predicate(predicate)
|
|
177
|
+
allowed = predicate.arity.zero? ? instance_exec(&predicate) : predicate.call(self)
|
|
178
|
+
return if allowed
|
|
179
|
+
|
|
180
|
+
render plain: "Forbidden", status: :forbidden
|
|
110
181
|
end
|
|
111
182
|
|
|
112
183
|
def fallback_http_basic_auth
|
|
@@ -119,7 +190,8 @@ module RailsPulse
|
|
|
119
190
|
logger.error "RailsPulse: No authentication method configured and RAILS_PULSE_PASSWORD not set. Access denied."
|
|
120
191
|
false
|
|
121
192
|
else
|
|
122
|
-
username
|
|
193
|
+
ActiveSupport::SecurityUtils.secure_compare(username.to_s, expected_username) &&
|
|
194
|
+
ActiveSupport::SecurityUtils.secure_compare(password.to_s, expected_password)
|
|
123
195
|
end
|
|
124
196
|
end
|
|
125
197
|
end
|
|
@@ -134,6 +206,7 @@ module RailsPulse
|
|
|
134
206
|
last_summary_at = RailsPulse::Summary.maximum(:updated_at)
|
|
135
207
|
@has_summaries = last_summary_at.present?
|
|
136
208
|
@summaries_stale = RailsPulse.configuration.warn_on_stale_summaries && @has_summaries && last_summary_at < 2.hours.ago
|
|
209
|
+
@route_backfill_pending = RailsPulse::Route.needs_action_backfill?
|
|
137
210
|
end
|
|
138
211
|
end
|
|
139
212
|
end
|
|
@@ -33,6 +33,15 @@ module RailsPulse
|
|
|
33
33
|
|
|
34
34
|
# System Health bar
|
|
35
35
|
@health_summary = RailsPulse::Dashboard::HealthSummary.new(disabled_tags: disabled_tags, show_non_tagged: show_non_tagged, period: @period).to_health_data
|
|
36
|
+
|
|
37
|
+
@storage_status = RailsPulse::Dashboard::StorageStatus.new
|
|
38
|
+
|
|
39
|
+
# Deployments panel — scoped to the same window as the chart markers so
|
|
40
|
+
# the panel and the markers drawn on the charts always agree.
|
|
41
|
+
deployments_in_range = RailsPulse::Deployment.for_range(Time.zone.at(@start_time), Time.zone.at(@end_time))
|
|
42
|
+
@deployment_count = deployments_in_range.count
|
|
43
|
+
@recent_deployments = deployments_in_range.recent.limit(4)
|
|
44
|
+
@last_deployment = RailsPulse::Deployment.recent.first
|
|
36
45
|
end
|
|
37
46
|
end
|
|
38
47
|
end
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
class DeploymentsController < ApplicationController
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# The API actions authenticate by token so CI can post without a session;
|
|
4
|
+
# the browsable pages use the dashboard's own authentication like every
|
|
5
|
+
# other page. Scoping the skip to create/finish means adding a UI does not
|
|
6
|
+
# widen the hole the 0.4.0 audit closed.
|
|
7
|
+
skip_before_action :authenticate_rails_pulse_user!, only: %i[create finish]
|
|
8
|
+
skip_before_action :verify_authenticity_token, only: %i[create finish]
|
|
9
|
+
before_action :authenticate_deployment_request!, only: %i[create finish]
|
|
10
|
+
|
|
11
|
+
def index
|
|
12
|
+
@ransack_query = Deployment.ransack(ransack_params)
|
|
13
|
+
@ransack_query.sorts = "started_at desc" if @ransack_query.sorts.empty?
|
|
14
|
+
@pagination, @table_data = paginate(@ransack_query.result, limit: session_pagination_limit)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
@deployment = Deployment.find(params[:id])
|
|
19
|
+
end
|
|
5
20
|
|
|
6
21
|
def create
|
|
7
22
|
deployment = RailsPulse::Deployment.new(
|
|
@@ -44,10 +59,16 @@ module RailsPulse
|
|
|
44
59
|
def authenticate_deployment_request!
|
|
45
60
|
token = RailsPulse.configuration.deployment_api_token
|
|
46
61
|
if token.present?
|
|
47
|
-
provided = request.headers["X-Rails-Pulse-Token"]
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
provided = request.headers["X-Rails-Pulse-Token"].to_s
|
|
63
|
+
unless ActiveSupport::SecurityUtils.secure_compare(provided, token)
|
|
64
|
+
render json: { error: "Unauthorized" }, status: :unauthorized
|
|
65
|
+
end
|
|
66
|
+
elsif RailsPulse.configuration.authentication_enabled
|
|
50
67
|
authenticate_rails_pulse_user!
|
|
68
|
+
else
|
|
69
|
+
# No token configured and authentication disabled — fail closed.
|
|
70
|
+
# Without a token there is no way to verify the caller.
|
|
71
|
+
render json: { error: "Unauthorized — set deployment_api_token or enable authentication" }, status: :unauthorized
|
|
51
72
|
end
|
|
52
73
|
end
|
|
53
74
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
class ExceptionOccurrencesController < ApplicationController
|
|
3
|
+
before_action :set_exception_group
|
|
4
|
+
before_action :set_occurrence
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def set_exception_group
|
|
12
|
+
@exception_group = ExceptionGroup.find(params[:exception_id])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def set_occurrence
|
|
16
|
+
@occurrence = @exception_group.occurrences.find(params[:id])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
class ExceptionsController < ApplicationController
|
|
3
|
+
include TimeRangeConcern
|
|
4
|
+
include TagFilterConcern
|
|
5
|
+
include DeploymentMarkersConcern
|
|
6
|
+
|
|
7
|
+
before_action :set_exception_group, only: %i[show update]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
ransack_params = self.ransack_params.dup
|
|
11
|
+
apply_default_status_filter!(ransack_params)
|
|
12
|
+
|
|
13
|
+
@ransack_query = ExceptionGroup.ransack(ransack_params)
|
|
14
|
+
@ransack_query.sorts = "last_seen_at desc" if @ransack_query.sorts.empty?
|
|
15
|
+
@pagination, @table_data = paginate(@ransack_query.result, limit: session_pagination_limit)
|
|
16
|
+
|
|
17
|
+
setup_frequency_view
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def show
|
|
21
|
+
occurrences = @exception_group.occurrences.order(occurred_at: :desc)
|
|
22
|
+
@latest_occurrence = occurrences.first
|
|
23
|
+
@pagination, @occurrences = paginate(occurrences, limit: session_pagination_limit)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
if params.key?(:preserve)
|
|
28
|
+
update_preserve
|
|
29
|
+
else
|
|
30
|
+
update_status
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# The index is a list of groups, not an aggregate table, so it does not use
|
|
37
|
+
# ChartTableConcern. It still needs a time range and a frequency series:
|
|
38
|
+
# without them the page can say what is failing but not whether it is
|
|
39
|
+
# getting worse, which is the question users actually arrive with.
|
|
40
|
+
def setup_frequency_view
|
|
41
|
+
# Timestamps, not Times, to match every other index controller — the
|
|
42
|
+
# shared concerns and helpers that read @start_time expect integers.
|
|
43
|
+
@start_time, @end_time, @selected_time_range, @time_diff_hours = setup_time_range
|
|
44
|
+
|
|
45
|
+
@period_type = @time_diff_hours <= 25 ? "hour" : "day"
|
|
46
|
+
period_days = window_days_for(@selected_time_range)
|
|
47
|
+
|
|
48
|
+
populate_deployment_markers
|
|
49
|
+
|
|
50
|
+
@occurrence_volume_chart = Exceptions::Charts::OccurrenceVolume.new(
|
|
51
|
+
start_time: Time.zone.at(@start_time),
|
|
52
|
+
end_time: Time.zone.at(@end_time),
|
|
53
|
+
period_type: @period_type
|
|
54
|
+
).to_chart_data
|
|
55
|
+
|
|
56
|
+
card_params = { period: period_days, period_type: @period_type }
|
|
57
|
+
|
|
58
|
+
@total_occurrences_metric_card = Exceptions::Cards::TotalOccurrences.new(**card_params).to_metric_card
|
|
59
|
+
@exception_rate_metric_card = Exceptions::Cards::ExceptionRate.new(**card_params).to_metric_card
|
|
60
|
+
@open_groups_metric_card = Exceptions::Cards::OpenGroups.new(**card_params).to_metric_card
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def set_exception_group
|
|
64
|
+
@exception_group = ExceptionGroup.find(params[:id])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The card copy ("compared to previous N days") has to name the range the
|
|
68
|
+
# user picked. Measuring the span instead reads one day long, because the
|
|
69
|
+
# range runs from the start of the first day to the end of the last.
|
|
70
|
+
def window_days_for(selected_range)
|
|
71
|
+
case selected_range.to_s
|
|
72
|
+
when "last_24_hours" then 1
|
|
73
|
+
when "last_7_days" then 7
|
|
74
|
+
when "last_14_days" then 14
|
|
75
|
+
when "last_30_days" then 30
|
|
76
|
+
else [ ((@end_time - @start_time) / 1.day.to_i).round, 1 ].max
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Exceptions are low-volume compared with requests, so a day is rarely
|
|
81
|
+
# enough to see a trend. Matches the jobs and queries defaults.
|
|
82
|
+
def default_time_range_key
|
|
83
|
+
:last_7_days
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Default the list to open groups. An explicit blank status ("All")
|
|
87
|
+
# clears the filter; any other value is passed through to ransack.
|
|
88
|
+
def apply_default_status_filter!(ransack_params)
|
|
89
|
+
if !ransack_params.key?(:status_eq) && !ransack_params.key?("status_eq")
|
|
90
|
+
ransack_params[:status_eq] = "open"
|
|
91
|
+
elsif ransack_params[:status_eq].blank? && ransack_params["status_eq"].blank?
|
|
92
|
+
ransack_params.delete(:status_eq)
|
|
93
|
+
ransack_params.delete("status_eq")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def update_preserve
|
|
98
|
+
@exception_group.update!(preserve: ActiveModel::Type::Boolean.new.cast(params[:preserve]))
|
|
99
|
+
flash[:notice] = @exception_group.preserve? ? "Exception preserved from cleanup." : "Exception no longer preserved."
|
|
100
|
+
redirect_to exception_path(@exception_group)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def update_status
|
|
104
|
+
new_status = params[:status]
|
|
105
|
+
unless ExceptionGroup::STATUSES.include?(new_status)
|
|
106
|
+
head :unprocessable_entity
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@exception_group.update!(
|
|
111
|
+
status: new_status,
|
|
112
|
+
resolved_at: new_status == "resolved" ? Time.current : nil
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
flash[:notice] = "Exception marked as #{new_status}."
|
|
116
|
+
redirect_to exception_path(@exception_group)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -6,7 +6,7 @@ module RailsPulse
|
|
|
6
6
|
before_action :set_run, only: :show
|
|
7
7
|
|
|
8
8
|
def index
|
|
9
|
-
@ransack_query = @job.runs.ransack(
|
|
9
|
+
@ransack_query = @job.runs.ransack(ransack_params)
|
|
10
10
|
@pagination, @runs = paginate(@ransack_query.result.order(occurred_at: :desc), limit: session_pagination_limit)
|
|
11
11
|
@table_data = @runs
|
|
12
12
|
end
|