solid_apm 0.11.1 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1c1e67891c00e76ce0f09899c23eaef403dd3daceeef92f58795525ee6a91e5
4
- data.tar.gz: ed1e539e7e0d2d3764dcc45b7ad131a8e6c6fbfc513ed6f80c4869350cab7c17
3
+ metadata.gz: 8842bfe3a66179077da889c3d8bae05eec4696d9fdd4839b54bac7a658d7f2c8
4
+ data.tar.gz: ee87c079419ad5d4fc45709c3534e6a2f406601efed22bac6a47e8eb819d5758
5
5
  SHA512:
6
- metadata.gz: a803be213c9785533bf87cf6fcb240926d47b63c47c3eff28aa5366349c3906bb9b46e17d7bd2190af3e1a35be2addf8140098f02611ff28b2b414bc85114047
7
- data.tar.gz: fb982b73450b544990308dbf6de65917708d06826d5f11744b0fcf0123745dde19ee259effdd989273569072956cffe3ea363022b368fcf514a43b94e40e9b59
6
+ metadata.gz: 934096ab8bf0b3f339588939a7b06d62b2a1cc86a54f1b426407a5aad3434a720b6ee0556341ba1d2950c36f29bb9987e34618c8b0d6181dd3e721853724818a
7
+ data.tar.gz: 12ceaacfe88e697a17277fb0421aaf10b2a90b97513ea1a214a0aeb7cbb09fa854a999d958cfe7e4160c05b8f52ef37daa841d9e54b01bc5b9565d8e7d968c42
data/README.md CHANGED
@@ -7,6 +7,10 @@ Rails engine to manage APM data without using a third party service.
7
7
  <img src="./docs/img_1.png" width="600px">
8
8
  <img src="./docs/img_2.png" width="600px">
9
9
 
10
+
11
+ > [!NOTE]
12
+ For a more **mature** solution (but dependent on Redis) have look to [rails_performance](https://github.com/igorkasyanchuk/rails_performance).
13
+
10
14
  ## Installation
11
15
 
12
16
  Add to your Gemfile:
@@ -95,6 +99,18 @@ The sampling is done per-thread using a round-robin counter, ensuring even distr
95
99
  This is useful for high-traffic applications where you want to reduce the volume of
96
100
  APM data while still maintaining representative performance insights.
97
101
 
102
+ ### Test Environment
103
+
104
+ **SolidAPM is automatically disabled in the test
105
+ environment** to prevent test pollution and improve test performance.
106
+
107
+ You can disable SolidAPM in other environments if needed:
108
+
109
+ ```ruby
110
+ # config/environments/staging.rb
111
+ SolidApm.enabled = false
112
+ ```
113
+
98
114
  ### Transaction Name Filtering
99
115
 
100
116
  Filter specific transactions by name using exact string matches or regular expressions:
@@ -101,4 +101,223 @@
101
101
  </div>
102
102
  <% end %>
103
103
 
104
- <%= javascript_include_tag 'solid_apm/time_range_form' %>
104
+ <script>
105
+ class TimeRangeForm {
106
+ constructor() {
107
+ this.form = document.getElementById('time-range-form');
108
+ this.relativeTab = document.getElementById('relative-tab');
109
+ this.absoluteTab = document.getElementById('absolute-tab');
110
+ this.relativePanel = document.getElementById('relative-panel');
111
+ this.absolutePanel = document.getElementById('absolute-panel');
112
+ this.customFromControl = document.getElementById('custom-from-control');
113
+ this.customToControl = document.getElementById('custom-to-control');
114
+
115
+ // Timezone handling
116
+ this.browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
117
+ this.timezoneOffset = new Date().getTimezoneOffset();
118
+
119
+ this.init();
120
+ }
121
+
122
+ init() {
123
+ this.setupEventListeners();
124
+ this.initializeFormState();
125
+ this.addTimezoneToForm();
126
+ this.adjustAbsoluteTimes();
127
+ }
128
+
129
+ setupEventListeners() {
130
+ if (this.form) {
131
+ this.form.addEventListener('submit', (e) => this.handleFormSubmit(e));
132
+ }
133
+ }
134
+
135
+ switchToRelative(event) {
136
+ event.preventDefault();
137
+
138
+ this.relativeTab.classList.add('is-primary');
139
+ this.absoluteTab.classList.remove('is-primary');
140
+ this.relativePanel.classList.remove('is-hidden');
141
+ this.absolutePanel.classList.add('is-hidden');
142
+
143
+ this.removeFields(['from_timestamp', 'to_timestamp']);
144
+ this.cleanupUrlParams(['from_timestamp', 'to_timestamp', 'from_datetime', 'to_datetime']);
145
+ }
146
+
147
+ switchToAbsolute(event) {
148
+ event.preventDefault();
149
+
150
+ this.absoluteTab.classList.add('is-primary');
151
+ this.relativeTab.classList.remove('is-primary');
152
+ this.absolutePanel.classList.remove('is-hidden');
153
+ this.relativePanel.classList.add('is-hidden');
154
+
155
+ this.cleanupUrlParams(['quick_range', 'from_value', 'from_unit', 'to_value', 'to_unit', 'quick_range_apply']);
156
+ }
157
+
158
+ handleQuickRangeChange(select) {
159
+ const isCustom = select.value === 'custom';
160
+
161
+ this.toggleVisibility(this.customFromControl, isCustom);
162
+ this.toggleVisibility(this.customToControl, isCustom);
163
+
164
+ if (!isCustom) {
165
+ this.applyQuickRange();
166
+ }
167
+ }
168
+
169
+ applyQuickRange() {
170
+ const quickRangeSelect = this.form.querySelector('[name="quick_range"]');
171
+ if (!quickRangeSelect || quickRangeSelect.value === 'custom') return;
172
+
173
+ this.removeFields(['from_datetime', 'to_datetime', 'from_timestamp', 'to_timestamp']);
174
+ this.addHiddenField('quick_range_apply', quickRangeSelect.value);
175
+ this.form.submit();
176
+ }
177
+
178
+ handleFormSubmit(event) {
179
+ const isAbsoluteMode = !this.absolutePanel.classList.contains('is-hidden');
180
+
181
+ if (isAbsoluteMode) {
182
+ this.handleAbsoluteModeSubmit();
183
+ } else {
184
+ this.handleRelativeModeSubmit();
185
+ }
186
+ }
187
+
188
+ handleAbsoluteModeSubmit() {
189
+ const fromDatetime = this.form.querySelector('[name="from_datetime"]');
190
+ const toDatetime = this.form.querySelector('[name="to_datetime"]');
191
+
192
+ if (fromDatetime?.value && toDatetime?.value) {
193
+ const fromTimestamp = Math.floor(new Date(fromDatetime.value).getTime() / 1000);
194
+ const toTimestamp = Math.floor(new Date(toDatetime.value).getTime() / 1000);
195
+
196
+ fromDatetime.disabled = true;
197
+ toDatetime.disabled = true;
198
+
199
+ this.addHiddenField('from_timestamp', fromTimestamp);
200
+ this.addHiddenField('to_timestamp', toTimestamp);
201
+ this.addHiddenField('browser_timezone', this.browserTimezone);
202
+ this.removeFields(['quick_range', 'from_value', 'from_unit', 'to_value', 'to_unit', 'quick_range_apply']);
203
+ }
204
+ }
205
+
206
+ handleRelativeModeSubmit() {
207
+ const quickRangeSelect = this.form.querySelector('[name="quick_range"]');
208
+ const quickRangeValue = quickRangeSelect?.value;
209
+
210
+ if (quickRangeValue && quickRangeValue !== 'custom') {
211
+ this.removeFields(['from_value', 'from_unit', 'to_value', 'to_unit', 'quick_range_apply']);
212
+ } else if (quickRangeValue === 'custom') {
213
+ this.removeFields(['quick_range_apply']);
214
+ }
215
+
216
+ this.removeFields(['from_datetime', 'to_datetime', 'from_timestamp', 'to_timestamp']);
217
+ }
218
+
219
+ initializeFormState() {
220
+ const urlParams = new URLSearchParams(window.location.search);
221
+ const hasCustomParams = urlParams.has('from_value') && urlParams.has('from_unit');
222
+ const hasQuickRange = urlParams.has('quick_range') && urlParams.get('quick_range') !== 'custom';
223
+ const quickRangeSelect = this.form.querySelector('[name="quick_range"]');
224
+
225
+ if (hasQuickRange) {
226
+ this.toggleVisibility(this.customFromControl, false);
227
+ this.toggleVisibility(this.customToControl, false);
228
+ } else if (hasCustomParams || urlParams.get('quick_range') === 'custom') {
229
+ if (quickRangeSelect) quickRangeSelect.value = 'custom';
230
+ this.toggleVisibility(this.customFromControl, true);
231
+ this.toggleVisibility(this.customToControl, true);
232
+ } else {
233
+ // Default state - show quick range only
234
+ this.toggleVisibility(this.customFromControl, false);
235
+ this.toggleVisibility(this.customToControl, false);
236
+ }
237
+ }
238
+
239
+ // Utility methods
240
+ removeFields(fieldNames) {
241
+ fieldNames.forEach(name => {
242
+ this.form.querySelectorAll(`[name="${name}"]`).forEach(field => field.remove());
243
+ });
244
+ }
245
+
246
+ addHiddenField(name, value) {
247
+ const input = document.createElement('input');
248
+ input.type = 'hidden';
249
+ input.name = name;
250
+ input.value = value;
251
+ this.form.appendChild(input);
252
+ }
253
+
254
+ toggleVisibility(element, show) {
255
+ if (!element) return;
256
+ element.classList.toggle('is-hidden', !show);
257
+ }
258
+
259
+ cleanupUrlParams(params) {
260
+ const url = new URL(window.location);
261
+ params.forEach(param => url.searchParams.delete(param));
262
+ window.history.replaceState({}, '', url);
263
+ }
264
+
265
+ // Timezone-related methods
266
+ addTimezoneToForm() {
267
+ // Add timezone information to form for server processing
268
+ this.addHiddenField('browser_timezone', this.browserTimezone);
269
+ }
270
+
271
+ adjustAbsoluteTimes() {
272
+ // Convert timestamps from URL to browser timezone for datetime-local inputs
273
+ const urlParams = new URLSearchParams(window.location.search);
274
+ const fromTimestamp = urlParams.get('from_timestamp');
275
+ const toTimestamp = urlParams.get('to_timestamp');
276
+
277
+ if (fromTimestamp && toTimestamp) {
278
+ const fromDatetime = this.form.querySelector('[name="from_datetime"]');
279
+ const toDatetime = this.form.querySelector('[name="to_datetime"]');
280
+
281
+ if (fromDatetime && toDatetime) {
282
+ // Convert UTC timestamps to local datetime strings
283
+ const fromDate = new Date(parseInt(fromTimestamp) * 1000);
284
+ const toDate = new Date(parseInt(toTimestamp) * 1000);
285
+
286
+ fromDatetime.value = this.formatDatetimeLocal(fromDate);
287
+ toDatetime.value = this.formatDatetimeLocal(toDate);
288
+ }
289
+ }
290
+ }
291
+
292
+ formatDatetimeLocal(date) {
293
+ // Format date for datetime-local input (YYYY-MM-DDTHH:MM)
294
+ const year = date.getFullYear();
295
+ const month = String(date.getMonth() + 1).padStart(2, '0');
296
+ const day = String(date.getDate()).padStart(2, '0');
297
+ const hours = String(date.getHours()).padStart(2, '0');
298
+ const minutes = String(date.getMinutes()).padStart(2, '0');
299
+
300
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
301
+ }
302
+ }
303
+
304
+ // Global functions for onclick handlers (maintaining backward compatibility)
305
+ let timeRangeFormInstance;
306
+
307
+ function switchToRelative(event) {
308
+ timeRangeFormInstance?.switchToRelative(event);
309
+ }
310
+
311
+ function switchToAbsolute(event) {
312
+ timeRangeFormInstance?.switchToAbsolute(event);
313
+ }
314
+
315
+ function handleQuickRangeChange(select) {
316
+ timeRangeFormInstance?.handleQuickRangeChange(select);
317
+ }
318
+
319
+ // Initialize when DOM is ready
320
+ document.addEventListener('DOMContentLoaded', function() {
321
+ timeRangeFormInstance = new TimeRangeForm();
322
+ });
323
+ </script>
@@ -4,40 +4,54 @@ module SolidApm
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace SolidApm
6
6
 
7
- config.app_middleware.use Middleware
7
+ initializer 'solid_apm.middleware', before: :build_middleware_stack do |app|
8
+ app.middleware.use SolidApm::Middleware if SolidApm.enabled
9
+ end
10
+
11
+ initializer 'solid_apm.assets' do |app|
12
+ # Add engine's assets to the load path for both Propshaft and Sprockets
13
+ if app.config.respond_to?(:assets)
14
+ app.config.assets.paths << root.join('app/assets/stylesheets')
15
+ app.config.assets.paths << root.join('app/assets/javascripts')
8
16
 
9
- initializer "solid_apm.assets.precompile" do |app|
10
- app.config.assets.precompile += %w( application.css application.js )
17
+ # For Sprockets
18
+ unless defined?(Propshaft)
19
+ app.config.assets.precompile += %w[
20
+ solid_apm/application.css
21
+ solid_apm/application.js
22
+ ]
23
+ end
24
+ end
11
25
  end
12
26
 
13
27
  begin
14
28
  # Mount the MCP server only if the main app added the fast_mcp in is Gemfile.
15
29
  require 'fast_mcp'
16
- initializer "solid_apm.mount_mcp_server" do |app|
17
- mcp_server_config = SolidApm.mcp_server_config.reverse_merge(
18
- name: 'solid-apm-mcp',
19
- version: '1.0.0',
20
- path: '/solid_apm/mcp'
21
- )
30
+ initializer 'solid_apm.mount_mcp_server' do |app|
31
+ mcp_server_config = SolidApm.mcp_server_config.reverse_merge(
32
+ name: 'solid-apm-mcp',
33
+ version: '1.0.0',
34
+ path: '/solid_apm/mcp'
35
+ )
22
36
 
23
- FastMcp.mount_in_rails(
24
- app,
25
- **mcp_server_config
26
- ) do |server|
27
- app.config.after_initialize do
28
- require_relative 'mcp/spans_for_transaction_tool'
29
- require_relative 'mcp/impactful_transactions_resource'
30
- server.register_resources(SolidApm::Mcp::ImpactfulTransactionsResource)
31
- server.register_tools(SolidApm::Mcp::SpansForTransactionTool)
37
+ FastMcp.mount_in_rails(
38
+ app,
39
+ **mcp_server_config
40
+ ) do |server|
41
+ app.config.after_initialize do
42
+ require_relative 'mcp/spans_for_transaction_tool'
43
+ require_relative 'mcp/impactful_transactions_resource'
44
+ server.register_resources(SolidApm::Mcp::ImpactfulTransactionsResource)
45
+ server.register_tools(SolidApm::Mcp::SpansForTransactionTool)
46
+ end
32
47
  end
33
48
  end
34
- end
35
49
  rescue LoadError
36
50
  # Ignored
37
51
  end
38
52
 
39
53
  config.after_initialize do
40
- SpanSubscriber::Base.subscribe!
54
+ SpanSubscriber::Base.subscribe! if SolidApm.enabled
41
55
  end
42
56
  end
43
57
  end
@@ -7,10 +7,12 @@ module SolidApm
7
7
  end
8
8
 
9
9
  def call(env)
10
+ return @app.call(env) unless SolidApm.enabled
11
+
10
12
  self.class.init_transaction
11
13
  status, headers, body = @app.call(env)
12
14
 
13
- env['rack.after_reply'] ||= []
15
+ env['rack.after_reply'] ||= []
14
16
  env['rack.after_reply'] << ->() do
15
17
  self.class.call
16
18
  rescue StandardError => e
@@ -25,8 +27,8 @@ module SolidApm
25
27
  SpanSubscriber::Base.transaction = nil
26
28
 
27
29
  if transaction.nil? ||
28
- transaction_filtered?(transaction.name) ||
29
- !Sampler.should_sample?
30
+ transaction_filtered?(transaction.name) ||
31
+ !Sampler.should_sample?
30
32
 
31
33
  SpanSubscriber::Base.spans = nil
32
34
  return
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidApm
4
+ class Railtie < Rails::Railtie
5
+ config.before_initialize do
6
+ # Always disable in test environment to prevent test pollution
7
+ SolidApm.enabled = false if Rails.env.test?
8
+ end
9
+ end
10
+ end
@@ -1,3 +1,3 @@
1
1
  module SolidApm
2
- VERSION = "0.11.1"
2
+ VERSION = "0.12.1"
3
3
  end
data/lib/solid_apm.rb CHANGED
@@ -4,6 +4,7 @@ require 'active_median'
4
4
  require 'apexcharts'
5
5
 
6
6
  require 'solid_apm/version'
7
+ require 'solid_apm/railtie'
7
8
  require 'solid_apm/engine'
8
9
  require 'solid_apm/sampler'
9
10
  require 'solid_apm/cleanup_service'
@@ -13,6 +14,7 @@ module SolidApm
13
14
  mattr_accessor :mcp_server_config, default: {}
14
15
  mattr_accessor :silence_active_record_logger, default: true
15
16
  mattr_accessor :transaction_sampling, default: 1
17
+ mattr_accessor :enabled, default: true
16
18
  mattr_accessor(
17
19
  :transaction_filters, default: [
18
20
  /^SolidApm::/,
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean-Francis Bastien
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-02-03 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: actionpack
@@ -119,7 +119,6 @@ files:
119
119
  - Rakefile
120
120
  - app/assets/config/solid_apm_manifest.js
121
121
  - app/assets/javascripts/solid_apm/application.js
122
- - app/assets/javascripts/solid_apm/time_range_form.js
123
122
  - app/assets/stylesheets/solid_apm/application.css
124
123
  - app/controllers/solid_apm/application_controller.rb
125
124
  - app/controllers/solid_apm/spans_controller.rb
@@ -152,6 +151,7 @@ files:
152
151
  - lib/solid_apm/mcp/impactful_transactions_resource.rb
153
152
  - lib/solid_apm/mcp/spans_for_transaction_tool.rb
154
153
  - lib/solid_apm/middleware.rb
154
+ - lib/solid_apm/railtie.rb
155
155
  - lib/solid_apm/sampler.rb
156
156
  - lib/solid_apm/version.rb
157
157
  - lib/tasks/solid_apm_tasks.rake
@@ -176,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
176
  - !ruby/object:Gem::Version
177
177
  version: '0'
178
178
  requirements: []
179
- rubygems_version: 3.6.9
179
+ rubygems_version: 3.6.2
180
180
  specification_version: 4
181
181
  summary: SolidApm is a DB base engine for Application Performance Monitoring.
182
182
  test_files: []
@@ -1,219 +0,0 @@
1
- class TimeRangeForm {
2
- constructor() {
3
- this.form = document.getElementById('time-range-form');
4
- this.relativeTab = document.getElementById('relative-tab');
5
- this.absoluteTab = document.getElementById('absolute-tab');
6
- this.relativePanel = document.getElementById('relative-panel');
7
- this.absolutePanel = document.getElementById('absolute-panel');
8
- this.customFromControl = document.getElementById('custom-from-control');
9
- this.customToControl = document.getElementById('custom-to-control');
10
-
11
- // Timezone handling
12
- this.browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
13
- this.timezoneOffset = new Date().getTimezoneOffset();
14
-
15
- this.init();
16
- }
17
-
18
- init() {
19
- this.setupEventListeners();
20
- this.initializeFormState();
21
- this.addTimezoneToForm();
22
- this.adjustAbsoluteTimes();
23
- }
24
-
25
- setupEventListeners() {
26
- if (this.form) {
27
- this.form.addEventListener('submit', (e) => this.handleFormSubmit(e));
28
- }
29
- }
30
-
31
- switchToRelative(event) {
32
- event.preventDefault();
33
-
34
- this.relativeTab.classList.add('is-primary');
35
- this.absoluteTab.classList.remove('is-primary');
36
- this.relativePanel.classList.remove('is-hidden');
37
- this.absolutePanel.classList.add('is-hidden');
38
-
39
- this.removeFields(['from_timestamp', 'to_timestamp']);
40
- this.cleanupUrlParams(['from_timestamp', 'to_timestamp', 'from_datetime', 'to_datetime']);
41
- }
42
-
43
- switchToAbsolute(event) {
44
- event.preventDefault();
45
-
46
- this.absoluteTab.classList.add('is-primary');
47
- this.relativeTab.classList.remove('is-primary');
48
- this.absolutePanel.classList.remove('is-hidden');
49
- this.relativePanel.classList.add('is-hidden');
50
-
51
- this.cleanupUrlParams(['quick_range', 'from_value', 'from_unit', 'to_value', 'to_unit', 'quick_range_apply']);
52
- }
53
-
54
- handleQuickRangeChange(select) {
55
- const isCustom = select.value === 'custom';
56
-
57
- this.toggleVisibility(this.customFromControl, isCustom);
58
- this.toggleVisibility(this.customToControl, isCustom);
59
-
60
- if (!isCustom) {
61
- this.applyQuickRange();
62
- }
63
- }
64
-
65
- applyQuickRange() {
66
- const quickRangeSelect = this.form.querySelector('[name="quick_range"]');
67
- if (!quickRangeSelect || quickRangeSelect.value === 'custom') return;
68
-
69
- this.removeFields(['from_datetime', 'to_datetime', 'from_timestamp', 'to_timestamp']);
70
- this.addHiddenField('quick_range_apply', quickRangeSelect.value);
71
- this.form.submit();
72
- }
73
-
74
- handleFormSubmit(event) {
75
- const isAbsoluteMode = !this.absolutePanel.classList.contains('is-hidden');
76
-
77
- if (isAbsoluteMode) {
78
- this.handleAbsoluteModeSubmit();
79
- } else {
80
- this.handleRelativeModeSubmit();
81
- }
82
- }
83
-
84
- handleAbsoluteModeSubmit() {
85
- const fromDatetime = this.form.querySelector('[name="from_datetime"]');
86
- const toDatetime = this.form.querySelector('[name="to_datetime"]');
87
-
88
- if (fromDatetime?.value && toDatetime?.value) {
89
- const fromTimestamp = Math.floor(new Date(fromDatetime.value).getTime() / 1000);
90
- const toTimestamp = Math.floor(new Date(toDatetime.value).getTime() / 1000);
91
-
92
- fromDatetime.disabled = true;
93
- toDatetime.disabled = true;
94
-
95
- this.addHiddenField('from_timestamp', fromTimestamp);
96
- this.addHiddenField('to_timestamp', toTimestamp);
97
- this.addHiddenField('browser_timezone', this.browserTimezone);
98
- this.removeFields(['quick_range', 'from_value', 'from_unit', 'to_value', 'to_unit', 'quick_range_apply']);
99
- }
100
- }
101
-
102
- handleRelativeModeSubmit() {
103
- const quickRangeSelect = this.form.querySelector('[name="quick_range"]');
104
- const quickRangeValue = quickRangeSelect?.value;
105
-
106
- if (quickRangeValue && quickRangeValue !== 'custom') {
107
- this.removeFields(['from_value', 'from_unit', 'to_value', 'to_unit', 'quick_range_apply']);
108
- } else if (quickRangeValue === 'custom') {
109
- this.removeFields(['quick_range_apply']);
110
- }
111
-
112
- this.removeFields(['from_datetime', 'to_datetime', 'from_timestamp', 'to_timestamp']);
113
- }
114
-
115
- initializeFormState() {
116
- const urlParams = new URLSearchParams(window.location.search);
117
- const hasCustomParams = urlParams.has('from_value') && urlParams.has('from_unit');
118
- const hasQuickRange = urlParams.has('quick_range') && urlParams.get('quick_range') !== 'custom';
119
- const quickRangeSelect = this.form.querySelector('[name="quick_range"]');
120
-
121
- if (hasQuickRange) {
122
- this.toggleVisibility(this.customFromControl, false);
123
- this.toggleVisibility(this.customToControl, false);
124
- } else if (hasCustomParams || urlParams.get('quick_range') === 'custom') {
125
- if (quickRangeSelect) quickRangeSelect.value = 'custom';
126
- this.toggleVisibility(this.customFromControl, true);
127
- this.toggleVisibility(this.customToControl, true);
128
- } else {
129
- // Default state - show quick range only
130
- this.toggleVisibility(this.customFromControl, false);
131
- this.toggleVisibility(this.customToControl, false);
132
- }
133
- }
134
-
135
- // Utility methods
136
- removeFields(fieldNames) {
137
- fieldNames.forEach(name => {
138
- this.form.querySelectorAll(`[name="${name}"]`).forEach(field => field.remove());
139
- });
140
- }
141
-
142
- addHiddenField(name, value) {
143
- const input = document.createElement('input');
144
- input.type = 'hidden';
145
- input.name = name;
146
- input.value = value;
147
- this.form.appendChild(input);
148
- }
149
-
150
- toggleVisibility(element, show) {
151
- if (!element) return;
152
- element.classList.toggle('is-hidden', !show);
153
- }
154
-
155
- cleanupUrlParams(params) {
156
- const url = new URL(window.location);
157
- params.forEach(param => url.searchParams.delete(param));
158
- window.history.replaceState({}, '', url);
159
- }
160
-
161
- // Timezone-related methods
162
- addTimezoneToForm() {
163
- // Add timezone information to form for server processing
164
- this.addHiddenField('browser_timezone', this.browserTimezone);
165
- }
166
-
167
- adjustAbsoluteTimes() {
168
- // Convert timestamps from URL to browser timezone for datetime-local inputs
169
- const urlParams = new URLSearchParams(window.location.search);
170
- const fromTimestamp = urlParams.get('from_timestamp');
171
- const toTimestamp = urlParams.get('to_timestamp');
172
-
173
- if (fromTimestamp && toTimestamp) {
174
- const fromDatetime = this.form.querySelector('[name="from_datetime"]');
175
- const toDatetime = this.form.querySelector('[name="to_datetime"]');
176
-
177
- if (fromDatetime && toDatetime) {
178
- // Convert UTC timestamps to local datetime strings
179
- const fromDate = new Date(parseInt(fromTimestamp) * 1000);
180
- const toDate = new Date(parseInt(toTimestamp) * 1000);
181
-
182
- fromDatetime.value = this.formatDatetimeLocal(fromDate);
183
- toDatetime.value = this.formatDatetimeLocal(toDate);
184
- }
185
- }
186
- }
187
-
188
- formatDatetimeLocal(date) {
189
- // Format date for datetime-local input (YYYY-MM-DDTHH:MM)
190
- const year = date.getFullYear();
191
- const month = String(date.getMonth() + 1).padStart(2, '0');
192
- const day = String(date.getDate()).padStart(2, '0');
193
- const hours = String(date.getHours()).padStart(2, '0');
194
- const minutes = String(date.getMinutes()).padStart(2, '0');
195
-
196
- return `${year}-${month}-${day}T${hours}:${minutes}`;
197
- }
198
-
199
- }
200
-
201
- // Global functions for onclick handlers (maintaining backward compatibility)
202
- let timeRangeFormInstance;
203
-
204
- function switchToRelative(event) {
205
- timeRangeFormInstance?.switchToRelative(event);
206
- }
207
-
208
- function switchToAbsolute(event) {
209
- timeRangeFormInstance?.switchToAbsolute(event);
210
- }
211
-
212
- function handleQuickRangeChange(select) {
213
- timeRangeFormInstance?.handleQuickRangeChange(select);
214
- }
215
-
216
- // Initialize when DOM is ready
217
- document.addEventListener('DOMContentLoaded', function() {
218
- timeRangeFormInstance = new TimeRangeForm();
219
- });