rails_pulse 0.1.0 → 0.1.2
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 +74 -178
- data/Rakefile +75 -173
- data/app/assets/stylesheets/rails_pulse/application.css +0 -12
- 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 +1 -1
- data/app/controllers/rails_pulse/application_controller.rb +8 -4
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -0
- data/app/controllers/rails_pulse/queries_controller.rb +65 -50
- data/app/controllers/rails_pulse/requests_controller.rb +24 -12
- data/app/controllers/rails_pulse/routes_controller.rb +59 -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 +6 -2
- data/app/helpers/rails_pulse/status_helper.rb +10 -4
- data/app/javascript/rails_pulse/controllers/index_controller.js +117 -33
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +17 -27
- 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 +20 -9
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +19 -7
- data/app/models/rails_pulse/operation.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +47 -23
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +33 -26
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +34 -45
- 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 +1 -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 -23
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +38 -45
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +34 -47
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +30 -25
- 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/summary_service.rb +199 -0
- data/app/views/layouts/rails_pulse/application.html.erb +4 -4
- data/app/views/rails_pulse/components/_empty_state.html.erb +11 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +10 -24
- data/app/views/rails_pulse/dashboard/index.html.erb +54 -36
- data/app/views/rails_pulse/queries/_show_table.html.erb +1 -1
- data/app/views/rails_pulse/queries/_table.html.erb +10 -12
- data/app/views/rails_pulse/queries/index.html.erb +41 -34
- data/app/views/rails_pulse/queries/show.html.erb +38 -31
- data/app/views/rails_pulse/requests/_operations.html.erb +32 -26
- data/app/views/rails_pulse/requests/_table.html.erb +1 -3
- data/app/views/rails_pulse/requests/index.html.erb +42 -34
- data/app/views/rails_pulse/routes/_table.html.erb +13 -13
- data/app/views/rails_pulse/routes/index.html.erb +43 -35
- data/app/views/rails_pulse/routes/show.html.erb +42 -35
- data/config/initializers/rails_pulse.rb +0 -12
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +54 -0
- data/db/rails_pulse_schema.rb +121 -0
- data/lib/generators/rails_pulse/install_generator.rb +41 -4
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +60 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +0 -12
- data/lib/rails_pulse/configuration.rb +6 -12
- data/lib/rails_pulse/engine.rb +0 -1
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/tasks/rails_pulse.rake +58 -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 +1 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +3 -3
- data/public/rails-pulse-assets/search.svg +43 -0
- metadata +28 -12
- data/app/controllers/rails_pulse/caches_controller.rb +0 -115
- data/app/helpers/rails_pulse/cached_component_helper.rb +0 -73
- 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,27 @@ 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
|
-
duration_gteq: @start_duration
|
41
|
+
base_params = ransack_params.except(:s).merge(
|
42
|
+
period_start_gteq: Time.at(@start_time),
|
43
|
+
period_start_lt: Time.at(@end_time)
|
38
44
|
)
|
45
|
+
|
46
|
+
# Only add duration filter if we have a meaningful threshold
|
47
|
+
base_params[:avg_duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
48
|
+
base_params
|
39
49
|
end
|
40
50
|
|
41
51
|
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
|
52
|
+
params = ransack_params.merge(
|
53
|
+
occurred_at_gteq: Time.at(@table_start_time),
|
54
|
+
occurred_at_lt: Time.at(@table_end_time)
|
46
55
|
)
|
56
|
+
params[:duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
57
|
+
params
|
47
58
|
end
|
48
59
|
|
49
60
|
def default_table_sort
|
@@ -52,13 +63,14 @@ module RailsPulse
|
|
52
63
|
|
53
64
|
def build_table_results
|
54
65
|
@ransack_query.result
|
55
|
-
.
|
66
|
+
.joins(:route)
|
56
67
|
.select(
|
57
68
|
"rails_pulse_requests.id",
|
58
69
|
"rails_pulse_requests.occurred_at",
|
59
70
|
"rails_pulse_requests.duration",
|
60
71
|
"rails_pulse_requests.status",
|
61
|
-
"rails_pulse_requests.route_id"
|
72
|
+
"rails_pulse_requests.route_id",
|
73
|
+
"rails_pulse_routes.path"
|
62
74
|
)
|
63
75
|
end
|
64
76
|
|
@@ -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,53 @@ 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
|
-
@ransack_query.result
|
87
|
+
@ransack_query.result
|
74
88
|
else
|
75
89
|
Routes::Tables::Index.new(
|
76
90
|
ransack_query: @ransack_query,
|
91
|
+
period_type: period_type,
|
77
92
|
start_time: @start_time,
|
78
93
|
params: params
|
79
94
|
).to_table
|
@@ -81,13 +96,33 @@ module RailsPulse
|
|
81
96
|
end
|
82
97
|
|
83
98
|
def duration_field
|
84
|
-
|
99
|
+
:avg_duration
|
85
100
|
end
|
86
101
|
|
87
102
|
def show_action?
|
88
103
|
action_name == "show"
|
89
104
|
end
|
90
105
|
|
106
|
+
def setup_table_data(ransack_params)
|
107
|
+
table_ransack_params = build_table_ransack_params(ransack_params)
|
108
|
+
@ransack_query = table_model.ransack(table_ransack_params)
|
109
|
+
|
110
|
+
# Only apply default sort if not using Routes::Tables::Index (which handles its own sorting)
|
111
|
+
if show_action?
|
112
|
+
@ransack_query.sorts = default_table_sort if @ransack_query.sorts.empty?
|
113
|
+
end
|
114
|
+
|
115
|
+
table_results = build_table_results
|
116
|
+
handle_pagination
|
117
|
+
|
118
|
+
@pagy, @table_data = pagy(table_results, limit: session_pagination_limit)
|
119
|
+
end
|
120
|
+
|
121
|
+
def handle_pagination
|
122
|
+
method = pagination_method
|
123
|
+
send(method, params[:limit]) if params[:limit].present?
|
124
|
+
end
|
125
|
+
|
91
126
|
def pagination_method
|
92
127
|
show_action? ? :set_pagination_limit : :store_pagination_limit
|
93
128
|
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
|
@@ -117,9 +117,13 @@ module RailsPulse
|
|
117
117
|
# Chart data is a hash like: { 1234567890 => { value: 123.45 } }
|
118
118
|
chart_timestamps = chart_data.keys
|
119
119
|
|
120
|
+
# Convert zoom parameters to integers (timestamps)
|
121
|
+
zoom_start_int = zoom_start.respond_to?(:to_i) ? zoom_start.to_i : zoom_start
|
122
|
+
zoom_end_int = zoom_end.respond_to?(:to_i) ? zoom_end.to_i : zoom_end
|
123
|
+
|
120
124
|
if chart_timestamps.any?
|
121
|
-
closest_start = chart_timestamps.min_by { |ts| (ts -
|
122
|
-
closest_end = chart_timestamps.min_by { |ts| (ts -
|
125
|
+
closest_start = chart_timestamps.min_by { |ts| (ts - zoom_start_int).abs }
|
126
|
+
closest_end = chart_timestamps.min_by { |ts| (ts - zoom_end_int).abs }
|
123
127
|
|
124
128
|
# Find the array indices of these timestamps
|
125
129
|
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
|
|
@@ -1,69 +1,108 @@
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
2
2
|
|
3
3
|
export default class extends Controller {
|
4
|
-
static targets = ["chart", "paginationLimit", "indexTable"]
|
4
|
+
static targets = ["chart", "paginationLimit", "indexTable"]
|
5
5
|
|
6
6
|
static values = {
|
7
7
|
chartId: String // The ID of the chart to be monitored
|
8
8
|
}
|
9
9
|
|
10
|
-
// Add
|
10
|
+
// Add properties for improved debouncing
|
11
11
|
lastTurboFrameRequestAt = 0;
|
12
|
+
pendingRequestTimeout = null;
|
13
|
+
pendingRequestData = null;
|
12
14
|
|
13
15
|
connect() {
|
14
16
|
// Listen for the custom event 'chart:initialized' to set up the chart.
|
15
17
|
// This event is sent from the RailsCharts library when the chart is ready.
|
16
18
|
this.handleChartInitialized = this.onChartInitialized.bind(this);
|
17
|
-
|
19
|
+
|
20
|
+
document.addEventListener('chart:rendered', this.handleChartInitialized);
|
18
21
|
|
19
22
|
// If the chart is already initialized (e.g., on back navigation), set up immediately
|
20
|
-
if (window.RailsCharts?.charts?.[this.chartIdValue]) {
|
23
|
+
if (window.RailsCharts?.charts?.[this.chartIdValue]) {
|
24
|
+
this.setup();
|
25
|
+
}
|
21
26
|
}
|
22
27
|
|
23
28
|
disconnect() {
|
24
29
|
// Remove the event listener from RailsCharts when the controller is disconnected
|
25
|
-
document.removeEventListener('chart:
|
30
|
+
document.removeEventListener('chart:rendered', this.handleChartInitialized);
|
26
31
|
|
27
32
|
// Remove chart event listeners if they exist
|
28
|
-
if (this.chartTarget) {
|
33
|
+
if (this.hasChartTarget && this.chartTarget) {
|
29
34
|
this.chartTarget.removeEventListener('mousedown', this.handleChartMouseDown);
|
30
35
|
this.chartTarget.removeEventListener('mouseup', this.handleChartMouseUp);
|
31
36
|
}
|
32
37
|
document.removeEventListener('mouseup', this.handleDocumentMouseUp);
|
38
|
+
|
39
|
+
// Clear any pending timeout
|
40
|
+
if (this.pendingRequestTimeout) {
|
41
|
+
clearTimeout(this.pendingRequestTimeout);
|
42
|
+
}
|
33
43
|
}
|
34
44
|
|
35
45
|
// After the chart is initialized, set up the event listeners and data tracking
|
36
46
|
onChartInitialized(event) {
|
37
|
-
if (event.detail.
|
47
|
+
if (event.detail.containerId === this.chartIdValue) {
|
48
|
+
this.setup();
|
49
|
+
}
|
38
50
|
}
|
39
51
|
|
40
52
|
setup() {
|
41
|
-
if (this.setupDone)
|
53
|
+
if (this.setupDone) {
|
54
|
+
return; // Prevent multiple setups
|
55
|
+
}
|
42
56
|
|
57
|
+
// We need both the chart target in DOM and the chart object from RailsCharts
|
58
|
+
let hasTarget = false;
|
59
|
+
try {
|
60
|
+
hasTarget = !!this.chartTarget;
|
61
|
+
} catch (e) {
|
62
|
+
hasTarget = false;
|
63
|
+
}
|
64
|
+
|
43
65
|
// Get the chart element which the RailsCharts library has created
|
44
66
|
this.chart = window.RailsCharts.charts[this.chartIdValue];
|
45
|
-
|
67
|
+
|
68
|
+
// Only proceed if we have BOTH the DOM target and the chart object
|
69
|
+
if (!hasTarget || !this.chart) {
|
70
|
+
return;
|
71
|
+
}
|
46
72
|
|
47
73
|
this.visibleData = this.getVisibleData();
|
48
74
|
this.setupChartEventListeners();
|
49
75
|
this.setupDone = true;
|
76
|
+
|
77
|
+
// Mark the chart as fully rendered for testing
|
78
|
+
if (hasTarget) {
|
79
|
+
document.getElementById(this.chartIdValue)?.setAttribute('data-chart-rendered', 'true');
|
80
|
+
}
|
50
81
|
}
|
51
82
|
|
52
83
|
// Add some event listeners to the chart so we can track the zoom changes
|
53
84
|
setupChartEventListeners() {
|
54
85
|
// When clicking on the chart, we want to store the current visible data so we can compare it later
|
55
|
-
this.handleChartMouseDown = () => {
|
86
|
+
this.handleChartMouseDown = () => {
|
87
|
+
this.visibleData = this.getVisibleData();
|
88
|
+
};
|
56
89
|
this.chartTarget.addEventListener('mousedown', this.handleChartMouseDown);
|
57
90
|
|
58
91
|
// When releasing the mouse button, we want to check if the visible data has changed
|
59
|
-
this.handleChartMouseUp = () => {
|
92
|
+
this.handleChartMouseUp = () => {
|
93
|
+
this.handleZoomChange();
|
94
|
+
};
|
60
95
|
this.chartTarget.addEventListener('mouseup', this.handleChartMouseUp);
|
61
96
|
|
62
97
|
// When the chart is zoomed, we want to check if the visible data has changed
|
63
|
-
this.chart.on('datazoom', () => {
|
98
|
+
this.chart.on('datazoom', () => {
|
99
|
+
this.handleZoomChange();
|
100
|
+
});
|
64
101
|
|
65
102
|
// When releasing the mouse button outside the chart, we want to check if the visible data has changed
|
66
|
-
this.handleDocumentMouseUp = () => {
|
103
|
+
this.handleDocumentMouseUp = () => {
|
104
|
+
this.handleZoomChange();
|
105
|
+
};
|
67
106
|
document.addEventListener('mouseup', this.handleDocumentMouseUp);
|
68
107
|
}
|
69
108
|
|
@@ -71,18 +110,37 @@ export default class extends Controller {
|
|
71
110
|
// The xAxis data and series data are sliced based on the start and end values of the dataZoom component.
|
72
111
|
// The series data will contain the actual data points that are visible in the chart.
|
73
112
|
getVisibleData() {
|
74
|
-
|
75
|
-
|
76
|
-
const xAxisData = currentOption.xAxis[0].data;
|
77
|
-
const seriesData = currentOption.series[0].data;
|
113
|
+
try {
|
114
|
+
const currentOption = this.chart.getOption();
|
78
115
|
|
79
|
-
|
80
|
-
|
116
|
+
if (!currentOption.dataZoom || currentOption.dataZoom.length === 0) {
|
117
|
+
return { xAxis: [], series: [] };
|
118
|
+
}
|
81
119
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
120
|
+
// Try to find the correct dataZoom component
|
121
|
+
let dataZoom = currentOption.dataZoom[1] || currentOption.dataZoom[0];
|
122
|
+
|
123
|
+
if (!currentOption.xAxis || !currentOption.xAxis[0] || !currentOption.xAxis[0].data) {
|
124
|
+
return { xAxis: [], series: [] };
|
125
|
+
}
|
126
|
+
|
127
|
+
if (!currentOption.series || !currentOption.series[0] || !currentOption.series[0].data) {
|
128
|
+
return { xAxis: [], series: [] };
|
129
|
+
}
|
130
|
+
|
131
|
+
const xAxisData = currentOption.xAxis[0].data;
|
132
|
+
const seriesData = currentOption.series[0].data;
|
133
|
+
|
134
|
+
const startValue = dataZoom.startValue || 0;
|
135
|
+
const endValue = dataZoom.endValue || xAxisData.length - 1;
|
136
|
+
|
137
|
+
return {
|
138
|
+
xAxis: xAxisData.slice(startValue, endValue + 1),
|
139
|
+
series: seriesData.slice(startValue, endValue + 1)
|
140
|
+
};
|
141
|
+
} catch (error) {
|
142
|
+
return { xAxis: [], series: [] };
|
143
|
+
}
|
86
144
|
}
|
87
145
|
|
88
146
|
// When the zoom level changes, we want to check if the visible data has changed
|
@@ -90,7 +148,10 @@ export default class extends Controller {
|
|
90
148
|
// we can update the table with the new data that is visible in the chart.
|
91
149
|
handleZoomChange() {
|
92
150
|
const newVisibleData = this.getVisibleData();
|
93
|
-
|
151
|
+
const newDataString = newVisibleData.xAxis.join();
|
152
|
+
const currentDataString = this.visibleData.xAxis.join();
|
153
|
+
|
154
|
+
if (newDataString !== currentDataString) {
|
94
155
|
this.visibleData = newVisibleData;
|
95
156
|
this.updateUrlWithZoomParams(newVisibleData);
|
96
157
|
this.sendTurboFrameRequest(newVisibleData);
|
@@ -124,14 +185,35 @@ export default class extends Controller {
|
|
124
185
|
window.history.replaceState({}, '', url);
|
125
186
|
}
|
126
187
|
|
127
|
-
//
|
128
|
-
// The server will then return the full page HTML with the updated table data wrapped in a turbo-frame.
|
129
|
-
// We will then replace the innerHTML of the turbo-frame with the new HTML.
|
188
|
+
// Improved debouncing with guaranteed final request
|
130
189
|
sendTurboFrameRequest(data) {
|
131
190
|
const now = Date.now();
|
132
|
-
|
133
|
-
|
134
|
-
|
191
|
+
const timeSinceLastRequest = now - this.lastTurboFrameRequestAt;
|
192
|
+
|
193
|
+
// Store the latest data for potential delayed execution
|
194
|
+
this.pendingRequestData = data;
|
195
|
+
|
196
|
+
// Clear any existing timeout
|
197
|
+
if (this.pendingRequestTimeout) {
|
198
|
+
clearTimeout(this.pendingRequestTimeout);
|
199
|
+
}
|
200
|
+
|
201
|
+
// If enough time has passed since last request, execute immediately
|
202
|
+
if (timeSinceLastRequest >= 1000) {
|
203
|
+
this.executeTurboFrameRequest(data);
|
204
|
+
} else {
|
205
|
+
// Otherwise, schedule execution for later to ensure final request goes through
|
206
|
+
const remainingTime = 1000 - timeSinceLastRequest;
|
207
|
+
this.pendingRequestTimeout = setTimeout(() => {
|
208
|
+
this.executeTurboFrameRequest(this.pendingRequestData);
|
209
|
+
this.pendingRequestTimeout = null;
|
210
|
+
}, remainingTime);
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
// Execute the actual AJAX request
|
215
|
+
executeTurboFrameRequest(data) {
|
216
|
+
this.lastTurboFrameRequestAt = Date.now();
|
135
217
|
|
136
218
|
// Start with the current page's URL
|
137
219
|
const url = new URL(window.location.href);
|
@@ -159,7 +241,9 @@ export default class extends Controller {
|
|
159
241
|
'Turbo-Frame': this.chartIdValue
|
160
242
|
}
|
161
243
|
})
|
162
|
-
.then(response =>
|
244
|
+
.then(response => {
|
245
|
+
return response.text();
|
246
|
+
})
|
163
247
|
.then(html => {
|
164
248
|
// Find the turbo-frame in the document using the target
|
165
249
|
const frame = this.indexTableTarget;
|
@@ -179,7 +263,7 @@ export default class extends Controller {
|
|
179
263
|
}
|
180
264
|
}
|
181
265
|
})
|
182
|
-
.catch(error => console.error('
|
266
|
+
.catch(error => console.error('[IndexController] Fetch error:', error));
|
183
267
|
}
|
184
268
|
|
185
269
|
// CSP-safe method to replace frame content using DOM methods
|
@@ -209,7 +293,7 @@ export default class extends Controller {
|
|
209
293
|
// Parse HTML safely
|
210
294
|
const parser = new DOMParser();
|
211
295
|
const doc = parser.parseFromString(html, 'text/html');
|
212
|
-
|
296
|
+
|
213
297
|
// Clear existing content
|
214
298
|
while (targetFrame.firstChild) {
|
215
299
|
targetFrame.removeChild(targetFrame.firstChild);
|
@@ -11,33 +11,25 @@ export default class extends Controller {
|
|
11
11
|
this.restorePaginationLimit()
|
12
12
|
}
|
13
13
|
|
14
|
-
// Update pagination limit
|
15
|
-
|
14
|
+
// Update pagination limit and refresh the turbo frame
|
15
|
+
updateLimit() {
|
16
16
|
const limit = this.limitTarget.value
|
17
17
|
|
18
|
-
// Save to session storage
|
18
|
+
// Save to session storage only - no server request needed
|
19
19
|
sessionStorage.setItem(this.storageKeyValue, limit)
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
// Reload the page to reflect the new pagination limit
|
34
|
-
// This preserves all current URL parameters including Ransack search params
|
35
|
-
window.location.reload()
|
36
|
-
} else {
|
37
|
-
throw new Error(`HTTP error! status: ${response.status}`)
|
38
|
-
}
|
39
|
-
} catch (error) {
|
40
|
-
console.error('Error updating pagination limit:', error)
|
21
|
+
// Find the closest turbo frame and reload it to apply new pagination
|
22
|
+
const turboFrame = this.element.closest('turbo-frame')
|
23
|
+
if (turboFrame) {
|
24
|
+
// Add the limit as a URL parameter so server picks it up
|
25
|
+
const currentUrl = new URL(window.location)
|
26
|
+
currentUrl.searchParams.set('limit', limit)
|
27
|
+
turboFrame.src = currentUrl.pathname + currentUrl.search
|
28
|
+
} else {
|
29
|
+
// Fallback to page reload if not within a turbo frame
|
30
|
+
const currentUrl = new URL(window.location)
|
31
|
+
currentUrl.searchParams.set('limit', limit)
|
32
|
+
window.location.href = currentUrl.pathname + currentUrl.search
|
41
33
|
}
|
42
34
|
}
|
43
35
|
|
@@ -60,10 +52,8 @@ export default class extends Controller {
|
|
60
52
|
// Only set if the current value is different (prevents unnecessary DOM updates)
|
61
53
|
if (this.limitTarget.value !== savedLimit) {
|
62
54
|
this.limitTarget.value = savedLimit
|
63
|
-
|
64
|
-
// Trigger a change event to ensure any other listeners are notified
|
65
|
-
this.limitTarget.dispatchEvent(new Event('change', { bubbles: true }))
|
55
|
+
// Don't trigger change event when restoring from session - prevents infinite loops
|
66
56
|
}
|
67
57
|
}
|
68
58
|
}
|
69
|
-
}
|
59
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
class BackfillSummariesJob < ApplicationJob
|
3
|
+
queue_as :low_priority
|
4
|
+
|
5
|
+
def perform(start_date, end_date, period_types = [ "hour", "day" ])
|
6
|
+
start_date = start_date.to_datetime
|
7
|
+
end_date = end_date.to_datetime
|
8
|
+
|
9
|
+
period_types.each do |period_type|
|
10
|
+
backfill_period(period_type, start_date, end_date)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def backfill_period(period_type, start_date, end_date)
|
17
|
+
current = Summary.normalize_period_start(period_type, start_date)
|
18
|
+
period_end = Summary.calculate_period_end(period_type, end_date)
|
19
|
+
|
20
|
+
while current <= period_end
|
21
|
+
Rails.logger.info "[RailsPulse] Backfilling #{period_type} summary for #{current}"
|
22
|
+
|
23
|
+
SummaryService.new(period_type, current).perform
|
24
|
+
|
25
|
+
current = advance_period(current, period_type)
|
26
|
+
|
27
|
+
# Add small delay to avoid overwhelming the database
|
28
|
+
sleep 0.1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def advance_period(time, period_type)
|
33
|
+
case period_type
|
34
|
+
when "hour" then time + 1.hour
|
35
|
+
when "day" then time + 1.day
|
36
|
+
when "week" then time + 1.week
|
37
|
+
when "month" then time + 1.month
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|