rails_pulse 0.1.1 → 0.1.3
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 +79 -177
- data/Rakefile +77 -2
- data/app/assets/images/rails_pulse/dashboard.png +0 -0
- data/app/assets/images/rails_pulse/request.png +0 -0
- data/app/assets/stylesheets/rails_pulse/application.css +28 -17
- data/app/assets/stylesheets/rails_pulse/components/badge.css +13 -0
- data/app/assets/stylesheets/rails_pulse/components/base.css +12 -2
- data/app/assets/stylesheets/rails_pulse/components/collapsible.css +30 -0
- data/app/assets/stylesheets/rails_pulse/components/popover.css +0 -1
- data/app/assets/stylesheets/rails_pulse/components/row.css +55 -3
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +23 -0
- data/app/controllers/concerns/chart_table_concern.rb +21 -4
- data/app/controllers/concerns/response_range_concern.rb +6 -3
- data/app/controllers/concerns/time_range_concern.rb +5 -10
- data/app/controllers/concerns/zoom_range_concern.rb +32 -1
- data/app/controllers/rails_pulse/application_controller.rb +13 -5
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -0
- data/app/controllers/rails_pulse/queries_controller.rb +111 -51
- data/app/controllers/rails_pulse/requests_controller.rb +37 -12
- data/app/controllers/rails_pulse/routes_controller.rb +98 -24
- data/app/helpers/rails_pulse/application_helper.rb +0 -1
- data/app/helpers/rails_pulse/chart_formatters.rb +3 -3
- data/app/helpers/rails_pulse/chart_helper.rb +21 -9
- data/app/helpers/rails_pulse/status_helper.rb +10 -4
- data/app/javascript/rails_pulse/application.js +34 -3
- data/app/javascript/rails_pulse/controllers/collapsible_controller.js +32 -0
- data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +2 -1
- data/app/javascript/rails_pulse/controllers/expandable_rows_controller.js +58 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +353 -39
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +17 -27
- data/app/javascript/rails_pulse/controllers/popover_controller.js +28 -4
- data/app/javascript/rails_pulse/controllers/table_sort_controller.js +14 -0
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +41 -0
- data/app/jobs/rails_pulse/summary_job.rb +53 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +28 -7
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +18 -22
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +18 -7
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +34 -41
- data/app/models/rails_pulse/operation.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +49 -25
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +40 -28
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +37 -43
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +23 -97
- data/app/models/rails_pulse/queries/tables/index.rb +74 -0
- data/app/models/rails_pulse/query.rb +47 -0
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +23 -84
- data/app/models/rails_pulse/route.rb +1 -6
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +45 -25
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +43 -45
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +36 -44
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +37 -27
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +23 -100
- data/app/models/rails_pulse/routes/tables/index.rb +57 -40
- data/app/models/rails_pulse/summary.rb +143 -0
- data/app/services/rails_pulse/analysis/backtrace_analyzer.rb +256 -0
- data/app/services/rails_pulse/analysis/base_analyzer.rb +67 -0
- data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +206 -0
- data/app/services/rails_pulse/analysis/index_recommendation_engine.rb +326 -0
- data/app/services/rails_pulse/analysis/n_plus_one_detector.rb +241 -0
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +146 -0
- data/app/services/rails_pulse/analysis/suggestion_generator.rb +217 -0
- data/app/services/rails_pulse/query_analysis_service.rb +125 -0
- data/app/services/rails_pulse/summary_service.rb +199 -0
- data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +0 -1
- data/app/views/layouts/rails_pulse/application.html.erb +4 -6
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +1 -1
- data/app/views/rails_pulse/components/_code_panel.html.erb +17 -3
- data/app/views/rails_pulse/components/_empty_state.html.erb +11 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +37 -28
- data/app/views/rails_pulse/components/_panel.html.erb +1 -1
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +5 -7
- data/app/views/rails_pulse/components/_table_head.html.erb +6 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +55 -37
- data/app/views/rails_pulse/operations/show.html.erb +17 -15
- data/app/views/rails_pulse/queries/_analysis_error.html.erb +15 -0
- data/app/views/rails_pulse/queries/_analysis_prompt.html.erb +27 -0
- data/app/views/rails_pulse/queries/_analysis_results.html.erb +87 -0
- data/app/views/rails_pulse/queries/_analysis_section.html.erb +39 -0
- data/app/views/rails_pulse/queries/_show_table.html.erb +2 -2
- data/app/views/rails_pulse/queries/_table.html.erb +11 -13
- data/app/views/rails_pulse/queries/index.html.erb +32 -28
- data/app/views/rails_pulse/queries/show.html.erb +45 -34
- data/app/views/rails_pulse/requests/_operations.html.erb +38 -45
- data/app/views/rails_pulse/requests/_table.html.erb +3 -3
- data/app/views/rails_pulse/requests/index.html.erb +33 -28
- data/app/views/rails_pulse/routes/_table.html.erb +14 -14
- data/app/views/rails_pulse/routes/index.html.erb +34 -29
- data/app/views/rails_pulse/routes/show.html.erb +43 -36
- data/config/initializers/rails_pulse.rb +0 -12
- data/config/routes.rb +5 -1
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +54 -0
- data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +13 -0
- data/db/rails_pulse_schema.rb +130 -0
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +65 -0
- data/lib/generators/rails_pulse/install_generator.rb +94 -4
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +60 -0
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +22 -0
- data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +19 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +0 -12
- data/lib/generators/rails_pulse/upgrade_generator.rb +225 -0
- data/lib/rails_pulse/configuration.rb +0 -11
- data/lib/rails_pulse/engine.rb +0 -1
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/tasks/rails_pulse.rake +77 -0
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +53 -53
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- data/public/rails-pulse-assets/search.svg +43 -0
- metadata +48 -14
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/images/rails_pulse/routes.png +0 -0
- data/app/controllers/rails_pulse/caches_controller.rb +0 -115
- data/app/helpers/rails_pulse/cached_component_helper.rb +0 -73
- data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +0 -67
- data/app/models/rails_pulse/component_cache_key.rb +0 -33
- data/app/views/rails_pulse/caches/show.html.erb +0 -9
- data/db/migrate/20250227235904_create_routes.rb +0 -12
- data/db/migrate/20250227235915_create_requests.rb +0 -19
- data/db/migrate/20250228000000_create_queries.rb +0 -14
- data/db/migrate/20250228000056_create_operations.rb +0 -24
- data/lib/rails_pulse/migration.rb +0 -29
@@ -5,6 +5,13 @@ module RailsPulse
|
|
5
5
|
before_action :set_request, only: :show
|
6
6
|
|
7
7
|
def index
|
8
|
+
unless turbo_frame_request?
|
9
|
+
@average_response_times_metric_card = RailsPulse::Routes::Cards::AverageResponseTimes.new(route: nil).to_metric_card
|
10
|
+
@percentile_response_times_metric_card = RailsPulse::Routes::Cards::PercentileResponseTimes.new(route: nil).to_metric_card
|
11
|
+
@request_count_totals_metric_card = RailsPulse::Routes::Cards::RequestCountTotals.new(route: nil).to_metric_card
|
12
|
+
@error_rate_per_route_metric_card = RailsPulse::Routes::Cards::ErrorRatePerRoute.new(route: nil).to_metric_card
|
13
|
+
end
|
14
|
+
|
8
15
|
setup_chart_and_table_data
|
9
16
|
end
|
10
17
|
|
@@ -15,7 +22,7 @@ module RailsPulse
|
|
15
22
|
private
|
16
23
|
|
17
24
|
def chart_model
|
18
|
-
|
25
|
+
Summary
|
19
26
|
end
|
20
27
|
|
21
28
|
def table_model
|
@@ -27,23 +34,29 @@ module RailsPulse
|
|
27
34
|
end
|
28
35
|
|
29
36
|
def chart_options
|
30
|
-
{
|
37
|
+
{}
|
31
38
|
end
|
32
39
|
|
33
40
|
def build_chart_ransack_params(ransack_params)
|
34
|
-
ransack_params.except(:s).merge(
|
35
|
-
|
36
|
-
|
37
|
-
|
41
|
+
base_params = ransack_params.except(:s).merge(
|
42
|
+
period_start_gteq: Time.at(@start_time),
|
43
|
+
period_start_lt: Time.at(@end_time),
|
44
|
+
summarizable_type_eq: "RailsPulse::Request",
|
45
|
+
summarizable_id_eq: 0
|
38
46
|
)
|
47
|
+
|
48
|
+
# Only add duration filter if we have a meaningful threshold
|
49
|
+
base_params[:avg_duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
50
|
+
base_params
|
39
51
|
end
|
40
52
|
|
41
53
|
def build_table_ransack_params(ransack_params)
|
42
|
-
ransack_params.merge(
|
43
|
-
occurred_at_gteq: @table_start_time,
|
44
|
-
occurred_at_lt: @table_end_time
|
45
|
-
duration_gteq: @start_duration
|
54
|
+
params = ransack_params.merge(
|
55
|
+
occurred_at_gteq: Time.at(@table_start_time),
|
56
|
+
occurred_at_lt: Time.at(@table_end_time)
|
46
57
|
)
|
58
|
+
params[:duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
59
|
+
params
|
47
60
|
end
|
48
61
|
|
49
62
|
def default_table_sort
|
@@ -51,15 +64,27 @@ module RailsPulse
|
|
51
64
|
end
|
52
65
|
|
53
66
|
def build_table_results
|
67
|
+
# Only show requests that belong to time periods where we have overall request summaries
|
68
|
+
# This ensures the table data is consistent with the chart data
|
54
69
|
@ransack_query.result
|
55
|
-
.
|
70
|
+
.joins(:route)
|
71
|
+
.joins(<<~SQL)
|
72
|
+
INNER JOIN rails_pulse_summaries ON
|
73
|
+
rails_pulse_summaries.summarizable_id = 0 AND
|
74
|
+
rails_pulse_summaries.summarizable_type = 'RailsPulse::Request' AND
|
75
|
+
rails_pulse_summaries.period_type = '#{period_type}' AND
|
76
|
+
rails_pulse_requests.occurred_at >= rails_pulse_summaries.period_start AND
|
77
|
+
rails_pulse_requests.occurred_at < rails_pulse_summaries.period_end
|
78
|
+
SQL
|
56
79
|
.select(
|
57
80
|
"rails_pulse_requests.id",
|
58
81
|
"rails_pulse_requests.occurred_at",
|
59
82
|
"rails_pulse_requests.duration",
|
60
83
|
"rails_pulse_requests.status",
|
61
|
-
"rails_pulse_requests.route_id"
|
84
|
+
"rails_pulse_requests.route_id",
|
85
|
+
"rails_pulse_routes.path"
|
62
86
|
)
|
87
|
+
.distinct
|
63
88
|
end
|
64
89
|
|
65
90
|
def set_request
|
@@ -5,21 +5,32 @@ module RailsPulse
|
|
5
5
|
before_action :set_route, only: :show
|
6
6
|
|
7
7
|
def index
|
8
|
+
setup_metric_cards
|
8
9
|
setup_chart_and_table_data
|
9
10
|
end
|
10
11
|
|
11
12
|
def show
|
13
|
+
setup_metric_cards
|
12
14
|
setup_chart_and_table_data
|
13
15
|
end
|
14
16
|
|
15
17
|
private
|
16
18
|
|
19
|
+
def setup_metric_cards
|
20
|
+
return if turbo_frame_request?
|
21
|
+
|
22
|
+
@average_query_times_metric_card = RailsPulse::Routes::Cards::AverageResponseTimes.new(route: @route).to_metric_card
|
23
|
+
@percentile_response_times_metric_card = RailsPulse::Routes::Cards::PercentileResponseTimes.new(route: @route).to_metric_card
|
24
|
+
@request_count_totals_metric_card = RailsPulse::Routes::Cards::RequestCountTotals.new(route: @route).to_metric_card
|
25
|
+
@error_rate_per_route_metric_card = RailsPulse::Routes::Cards::ErrorRatePerRoute.new(route: @route).to_metric_card
|
26
|
+
end
|
27
|
+
|
17
28
|
def chart_model
|
18
|
-
|
29
|
+
Summary
|
19
30
|
end
|
20
31
|
|
21
32
|
def table_model
|
22
|
-
show_action? ? Request :
|
33
|
+
show_action? ? Request : Summary
|
23
34
|
end
|
24
35
|
|
25
36
|
def chart_class
|
@@ -31,49 +42,71 @@ module RailsPulse
|
|
31
42
|
end
|
32
43
|
|
33
44
|
def build_chart_ransack_params(ransack_params)
|
34
|
-
base_params = ransack_params.except(:s).merge(
|
45
|
+
base_params = ransack_params.except(:s).merge(
|
46
|
+
period_start_gteq: Time.at(@start_time),
|
47
|
+
period_start_lt: Time.at(@end_time)
|
48
|
+
)
|
49
|
+
|
50
|
+
# Only add duration filter if we have a meaningful threshold
|
51
|
+
base_params[:avg_duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
35
52
|
|
36
53
|
if show_action?
|
37
|
-
base_params.merge(
|
38
|
-
route_id_eq: @route.id,
|
39
|
-
occurred_at_gteq: @start_time,
|
40
|
-
occurred_at_lt: @end_time
|
41
|
-
)
|
54
|
+
base_params.merge(summarizable_id_eq: @route.id)
|
42
55
|
else
|
43
|
-
base_params
|
44
|
-
requests_occurred_at_gteq: @start_time,
|
45
|
-
requests_occurred_at_lt: @end_time
|
46
|
-
)
|
56
|
+
base_params
|
47
57
|
end
|
48
58
|
end
|
49
59
|
|
50
60
|
def build_table_ransack_params(ransack_params)
|
51
|
-
base_params = ransack_params.merge(duration_field => @start_duration)
|
52
|
-
|
53
61
|
if show_action?
|
54
|
-
|
55
|
-
|
56
|
-
occurred_at_gteq: @table_start_time,
|
57
|
-
occurred_at_lt: @table_end_time
|
62
|
+
# For Request model on show page
|
63
|
+
params = ransack_params.merge(
|
64
|
+
occurred_at_gteq: Time.at(@table_start_time),
|
65
|
+
occurred_at_lt: Time.at(@table_end_time),
|
66
|
+
route_id_eq: @route.id
|
58
67
|
)
|
68
|
+
params[:duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
69
|
+
params
|
59
70
|
else
|
60
|
-
|
61
|
-
|
62
|
-
|
71
|
+
# For Summary model on index page
|
72
|
+
params = ransack_params.merge(
|
73
|
+
period_start_gteq: Time.at(@table_start_time),
|
74
|
+
period_start_lt: Time.at(@table_end_time)
|
63
75
|
)
|
76
|
+
params[:avg_duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
77
|
+
params
|
64
78
|
end
|
65
79
|
end
|
66
80
|
|
67
81
|
def default_table_sort
|
68
|
-
show_action? ? "occurred_at desc" : "
|
82
|
+
show_action? ? "occurred_at desc" : "avg_duration desc"
|
69
83
|
end
|
70
84
|
|
71
85
|
def build_table_results
|
72
86
|
if show_action?
|
73
|
-
|
87
|
+
# Only show requests that belong to time periods where we have route summaries
|
88
|
+
# This ensures the table data is consistent with the chart data
|
89
|
+
base_query = @ransack_query.result
|
90
|
+
.joins(<<~SQL)
|
91
|
+
INNER JOIN rails_pulse_summaries ON
|
92
|
+
rails_pulse_summaries.summarizable_id = rails_pulse_requests.route_id AND
|
93
|
+
rails_pulse_summaries.summarizable_type = 'RailsPulse::Route' AND
|
94
|
+
rails_pulse_summaries.period_type = '#{period_type}' AND
|
95
|
+
rails_pulse_requests.occurred_at >= rails_pulse_summaries.period_start AND
|
96
|
+
rails_pulse_requests.occurred_at < rails_pulse_summaries.period_end
|
97
|
+
SQL
|
98
|
+
|
99
|
+
# For PostgreSQL compatibility with DISTINCT + ORDER BY
|
100
|
+
# we need to include computed columns in SELECT when ordering by them
|
101
|
+
if ordering_by_computed_column?
|
102
|
+
base_query.select("rails_pulse_requests.*, #{status_indicator_sql} as status_indicator_value").distinct
|
103
|
+
else
|
104
|
+
base_query.distinct
|
105
|
+
end
|
74
106
|
else
|
75
107
|
Routes::Tables::Index.new(
|
76
108
|
ransack_query: @ransack_query,
|
109
|
+
period_type: period_type,
|
77
110
|
start_time: @start_time,
|
78
111
|
params: params
|
79
112
|
).to_table
|
@@ -81,13 +114,33 @@ module RailsPulse
|
|
81
114
|
end
|
82
115
|
|
83
116
|
def duration_field
|
84
|
-
|
117
|
+
:avg_duration
|
85
118
|
end
|
86
119
|
|
87
120
|
def show_action?
|
88
121
|
action_name == "show"
|
89
122
|
end
|
90
123
|
|
124
|
+
def setup_table_data(ransack_params)
|
125
|
+
table_ransack_params = build_table_ransack_params(ransack_params)
|
126
|
+
@ransack_query = table_model.ransack(table_ransack_params)
|
127
|
+
|
128
|
+
# Only apply default sort if not using Routes::Tables::Index (which handles its own sorting)
|
129
|
+
if show_action?
|
130
|
+
@ransack_query.sorts = default_table_sort if @ransack_query.sorts.empty?
|
131
|
+
end
|
132
|
+
|
133
|
+
table_results = build_table_results
|
134
|
+
handle_pagination
|
135
|
+
|
136
|
+
@pagy, @table_data = pagy(table_results, limit: session_pagination_limit)
|
137
|
+
end
|
138
|
+
|
139
|
+
def handle_pagination
|
140
|
+
method = pagination_method
|
141
|
+
send(method, params[:limit]) if params[:limit].present?
|
142
|
+
end
|
143
|
+
|
91
144
|
def pagination_method
|
92
145
|
show_action? ? :set_pagination_limit : :store_pagination_limit
|
93
146
|
end
|
@@ -95,5 +148,26 @@ module RailsPulse
|
|
95
148
|
def set_route
|
96
149
|
@route = Route.find(params[:id])
|
97
150
|
end
|
151
|
+
|
152
|
+
def ordering_by_computed_column?
|
153
|
+
# Check if we're ordering by status_indicator (computed column)
|
154
|
+
@ransack_query.sorts.any? { |sort| sort.name == "status_indicator" }
|
155
|
+
end
|
156
|
+
|
157
|
+
def status_indicator_sql
|
158
|
+
# Same logic as in the Request model's ransacker
|
159
|
+
config = RailsPulse.configuration rescue nil
|
160
|
+
thresholds = config&.request_thresholds || { slow: 500, very_slow: 1000, critical: 2000 }
|
161
|
+
slow = thresholds[:slow] || 500
|
162
|
+
very_slow = thresholds[:very_slow] || 1000
|
163
|
+
critical = thresholds[:critical] || 2000
|
164
|
+
|
165
|
+
"CASE
|
166
|
+
WHEN rails_pulse_requests.duration < #{slow} THEN 0
|
167
|
+
WHEN rails_pulse_requests.duration < #{very_slow} THEN 1
|
168
|
+
WHEN rails_pulse_requests.duration < #{critical} THEN 2
|
169
|
+
ELSE 3
|
170
|
+
END"
|
171
|
+
end
|
98
172
|
end
|
99
173
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module RailsPulse
|
2
2
|
module ChartFormatters
|
3
|
-
def self.
|
3
|
+
def self.period_as_time_or_date(time_diff_hours)
|
4
4
|
if time_diff_hours <= 25
|
5
5
|
<<~JS
|
6
6
|
function(value) {
|
@@ -25,7 +25,7 @@ module RailsPulse
|
|
25
25
|
const data = params[0];
|
26
26
|
const date = new Date(data.axisValue * 1000);
|
27
27
|
const dateString = date.getHours().toString().padStart(2, '0') + ':00';
|
28
|
-
return `${dateString} <br /> ${data.marker} ${parseInt(data.data
|
28
|
+
return `${dateString} <br /> ${data.marker} ${parseInt(data.data)} ms`;
|
29
29
|
}
|
30
30
|
JS
|
31
31
|
else
|
@@ -34,7 +34,7 @@ module RailsPulse
|
|
34
34
|
const data = params[0];
|
35
35
|
const date = new Date(data.axisValue * 1000);
|
36
36
|
const dateString = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
37
|
-
return `${dateString} <br /> ${data.marker} ${parseInt(data.data
|
37
|
+
return `${dateString} <br /> ${data.marker} ${parseInt(data.data)} ms`;
|
38
38
|
}
|
39
39
|
JS
|
40
40
|
end
|
@@ -63,16 +63,24 @@ module RailsPulse
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def sparkline_chart_options
|
66
|
+
# Compact sparkline columns that fill the canvas with no axes/labels/gaps
|
66
67
|
base_chart_options.deep_merge({
|
67
68
|
series: {
|
68
|
-
type: "
|
69
|
-
|
70
|
-
|
71
|
-
|
69
|
+
type: "bar",
|
70
|
+
itemStyle: { borderRadius: [ 2, 2, 0, 0 ] },
|
71
|
+
barCategoryGap: "10%",
|
72
|
+
barGap: "0%"
|
72
73
|
},
|
73
|
-
yAxis: { show: false },
|
74
|
-
xAxis: {
|
75
|
-
|
74
|
+
yAxis: { show: false, splitLine: { show: false } },
|
75
|
+
xAxis: {
|
76
|
+
type: "category",
|
77
|
+
boundaryGap: true,
|
78
|
+
axisLine: { show: false },
|
79
|
+
axisTick: { show: false },
|
80
|
+
splitLine: { show: false },
|
81
|
+
axisLabel: { show: false }
|
82
|
+
},
|
83
|
+
grid: { left: 0, right: 0, top: 0, bottom: 0, containLabel: false, show: false }
|
76
84
|
})
|
77
85
|
end
|
78
86
|
|
@@ -117,9 +125,13 @@ module RailsPulse
|
|
117
125
|
# Chart data is a hash like: { 1234567890 => { value: 123.45 } }
|
118
126
|
chart_timestamps = chart_data.keys
|
119
127
|
|
128
|
+
# Convert zoom parameters to integers (timestamps)
|
129
|
+
zoom_start_int = zoom_start.respond_to?(:to_i) ? zoom_start.to_i : zoom_start
|
130
|
+
zoom_end_int = zoom_end.respond_to?(:to_i) ? zoom_end.to_i : zoom_end
|
131
|
+
|
120
132
|
if chart_timestamps.any?
|
121
|
-
closest_start = chart_timestamps.min_by { |ts| (ts -
|
122
|
-
closest_end = chart_timestamps.min_by { |ts| (ts -
|
133
|
+
closest_start = chart_timestamps.min_by { |ts| (ts - zoom_start_int).abs }
|
134
|
+
closest_end = chart_timestamps.min_by { |ts| (ts - zoom_end_int).abs }
|
123
135
|
|
124
136
|
# Find the array indices of these timestamps
|
125
137
|
start_index = chart_timestamps.index(closest_start)
|
@@ -256,13 +256,19 @@ module RailsPulse
|
|
256
256
|
|
257
257
|
def event_color(operation_type)
|
258
258
|
case operation_type
|
259
|
-
when "sql"
|
260
|
-
|
261
|
-
when "
|
262
|
-
|
259
|
+
when "sql"
|
260
|
+
"#d27d6b"
|
261
|
+
when "template", "partial", "layout", "collection"
|
262
|
+
"#6c7ab9"
|
263
|
+
when "controller"
|
264
|
+
"#5ba6b0"
|
265
|
+
else
|
266
|
+
"#a6a6a6"
|
263
267
|
end
|
264
268
|
end
|
265
269
|
|
270
|
+
|
271
|
+
|
266
272
|
def duration_options(type = :route)
|
267
273
|
thresholds = RailsPulse.configuration.public_send("#{type}_thresholds")
|
268
274
|
|
@@ -16,7 +16,9 @@ import ColorSchemeController from "./controllers/color_scheme_controller";
|
|
16
16
|
import PaginationController from "./controllers/pagination_controller";
|
17
17
|
import TimezoneController from "./controllers/timezone_controller";
|
18
18
|
import IconController from "./controllers/icon_controller";
|
19
|
-
import
|
19
|
+
import ExpandableRowsController from "./controllers/expandable_rows_controller";
|
20
|
+
import CollapsibleController from "./controllers/collapsible_controller";
|
21
|
+
import TableSortController from "./controllers/table_sort_controller";
|
20
22
|
|
21
23
|
const application = Application.start();
|
22
24
|
|
@@ -41,7 +43,9 @@ application.register("rails-pulse--color-scheme", ColorSchemeController);
|
|
41
43
|
application.register("rails-pulse--pagination", PaginationController);
|
42
44
|
application.register("rails-pulse--timezone", TimezoneController);
|
43
45
|
application.register("rails-pulse--icon", IconController);
|
44
|
-
application.register("rails-pulse--expandable-
|
46
|
+
application.register("rails-pulse--expandable-rows", ExpandableRowsController);
|
47
|
+
application.register("rails-pulse--collapsible", CollapsibleController);
|
48
|
+
application.register("rails-pulse--table-sort", TableSortController);
|
45
49
|
|
46
50
|
// Ensure Turbo Frames are loaded after page load
|
47
51
|
document.addEventListener('DOMContentLoaded', () => {
|
@@ -95,6 +99,32 @@ window.addEventListener('resize', function() {
|
|
95
99
|
}
|
96
100
|
});
|
97
101
|
|
102
|
+
// Apply axis label colors based on current color scheme
|
103
|
+
function applyChartAxisLabelColors() {
|
104
|
+
if (!window.RailsCharts || !window.RailsCharts.charts) return;
|
105
|
+
const scheme = document.documentElement.getAttribute('data-color-scheme');
|
106
|
+
const isDark = scheme === 'dark';
|
107
|
+
const axisColor = isDark ? '#ffffff' : '#999999';
|
108
|
+
Object.keys(window.RailsCharts.charts).forEach(function(chartID) {
|
109
|
+
const chart = window.RailsCharts.charts[chartID];
|
110
|
+
try {
|
111
|
+
chart.setOption({
|
112
|
+
xAxis: { axisLabel: { color: axisColor } },
|
113
|
+
yAxis: { axisLabel: { color: axisColor } }
|
114
|
+
});
|
115
|
+
} catch (e) {
|
116
|
+
// noop
|
117
|
+
}
|
118
|
+
});
|
119
|
+
}
|
120
|
+
|
121
|
+
// Initial apply after charts initialize and on scheme changes
|
122
|
+
document.addEventListener('DOMContentLoaded', () => {
|
123
|
+
// run shortly after load to allow charts to initialize
|
124
|
+
setTimeout(applyChartAxisLabelColors, 50);
|
125
|
+
});
|
126
|
+
document.addEventListener('rails-pulse:color-scheme-changed', applyChartAxisLabelColors);
|
127
|
+
|
98
128
|
// Global function to initialize Rails Charts in any container.
|
99
129
|
// This is needed as we render Rails Charts in Turbo Frames.
|
100
130
|
window.initializeChartsInContainer = function(containerId) {
|
@@ -108,6 +138,8 @@ window.initializeChartsInContainer = function(containerId) {
|
|
108
138
|
window[match[1]]();
|
109
139
|
}
|
110
140
|
});
|
141
|
+
// ensure colors are correct for any charts initialized in this container
|
142
|
+
setTimeout(applyChartAxisLabelColors, 10);
|
111
143
|
});
|
112
144
|
};
|
113
145
|
|
@@ -116,4 +148,3 @@ window.RailsPulse = {
|
|
116
148
|
application,
|
117
149
|
version: "1.0.0"
|
118
150
|
};
|
119
|
-
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["content", "toggle"]
|
5
|
+
static classes = ["collapsed"]
|
6
|
+
|
7
|
+
connect() {
|
8
|
+
this.collapse()
|
9
|
+
}
|
10
|
+
|
11
|
+
toggle() {
|
12
|
+
if (this.element.classList.contains(this.collapsedClass)) {
|
13
|
+
this.expand()
|
14
|
+
} else {
|
15
|
+
this.collapse()
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
collapse() {
|
20
|
+
this.element.classList.add(this.collapsedClass)
|
21
|
+
if (this.hasToggleTarget) {
|
22
|
+
this.toggleTarget.textContent = "show more"
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
expand() {
|
27
|
+
this.element.classList.remove(this.collapsedClass)
|
28
|
+
if (this.hasToggleTarget) {
|
29
|
+
this.toggleTarget.textContent = "show less"
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
@@ -13,8 +13,9 @@ export default class extends Controller {
|
|
13
13
|
toggle(event) {
|
14
14
|
event.preventDefault()
|
15
15
|
const current = this.html.getAttribute("data-color-scheme") === "dark" ? "light" : "dark"
|
16
|
-
console.log("Toggling color scheme to", current)
|
17
16
|
this.html.setAttribute("data-color-scheme", current)
|
18
17
|
localStorage.setItem(this.storageKey, current)
|
18
|
+
// Notify listeners (e.g., charts) that scheme changed
|
19
|
+
document.dispatchEvent(new CustomEvent('rails-pulse:color-scheme-changed', { detail: { scheme: current }}))
|
19
20
|
}
|
20
21
|
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
toggle(event) {
|
5
|
+
// Delegate clicks from tbody to the nearest row
|
6
|
+
const triggerRow = event.target.closest('tr')
|
7
|
+
if (!triggerRow || triggerRow.closest('tbody') !== this.element) return
|
8
|
+
|
9
|
+
// Ignore clicks on the details row itself
|
10
|
+
if (triggerRow.classList.contains('operation-details-row')) return
|
11
|
+
|
12
|
+
// Do not toggle when clicking the final Actions column
|
13
|
+
const clickedCell = event.target.closest('td,th')
|
14
|
+
if (clickedCell && clickedCell.parentElement === triggerRow) {
|
15
|
+
const isLastCell = clickedCell.cellIndex === (triggerRow.cells.length - 1)
|
16
|
+
if (isLastCell) return
|
17
|
+
}
|
18
|
+
|
19
|
+
event.preventDefault()
|
20
|
+
event.stopPropagation()
|
21
|
+
|
22
|
+
const detailsRow = triggerRow.nextElementSibling
|
23
|
+
if (!detailsRow || detailsRow.tagName !== 'TR' || !detailsRow.classList.contains('operation-details-row')) return
|
24
|
+
|
25
|
+
const isHidden = detailsRow.classList.contains('hidden')
|
26
|
+
if (isHidden) {
|
27
|
+
this.expand(triggerRow, detailsRow)
|
28
|
+
} else {
|
29
|
+
this.collapse(triggerRow, detailsRow)
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
expand(triggerRow, detailsRow) {
|
34
|
+
detailsRow.classList.remove('hidden')
|
35
|
+
|
36
|
+
// Rotate chevron to point down
|
37
|
+
const chevron = triggerRow.querySelector('.chevron')
|
38
|
+
if (chevron) chevron.style.transform = 'rotate(90deg)'
|
39
|
+
|
40
|
+
triggerRow.classList.add('expanded')
|
41
|
+
|
42
|
+
// Lazy load operation details once
|
43
|
+
const frame = detailsRow.querySelector('turbo-frame')
|
44
|
+
if (frame && !frame.getAttribute('src')) {
|
45
|
+
const url = frame.dataset.operationUrl
|
46
|
+
if (url) frame.setAttribute('src', url)
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
collapse(triggerRow, detailsRow) {
|
51
|
+
detailsRow.classList.add('hidden')
|
52
|
+
|
53
|
+
const chevron = triggerRow.querySelector('.chevron')
|
54
|
+
if (chevron) chevron.style.transform = 'rotate(0deg)'
|
55
|
+
|
56
|
+
triggerRow.classList.remove('expanded')
|
57
|
+
}
|
58
|
+
}
|