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 +4 -4
- data/README.md +16 -0
- data/app/views/solid_apm/application/_time_range_form.html.erb +220 -1
- data/lib/solid_apm/engine.rb +34 -20
- data/lib/solid_apm/middleware.rb +5 -3
- data/lib/solid_apm/railtie.rb +10 -0
- data/lib/solid_apm/version.rb +1 -1
- data/lib/solid_apm.rb +2 -0
- metadata +4 -4
- data/app/assets/javascripts/solid_apm/time_range_form.js +0 -219
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8842bfe3a66179077da889c3d8bae05eec4696d9fdd4839b54bac7a658d7f2c8
|
|
4
|
+
data.tar.gz: ee87c079419ad5d4fc45709c3534e6a2f406601efed22bac6a47e8eb819d5758
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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>
|
data/lib/solid_apm/engine.rb
CHANGED
|
@@ -4,40 +4,54 @@ module SolidApm
|
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
isolate_namespace SolidApm
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
data/lib/solid_apm/middleware.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
+
transaction_filtered?(transaction.name) ||
|
|
31
|
+
!Sampler.should_sample?
|
|
30
32
|
|
|
31
33
|
SpanSubscriber::Base.spans = nil
|
|
32
34
|
return
|
data/lib/solid_apm/version.rb
CHANGED
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.
|
|
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:
|
|
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.
|
|
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
|
-
});
|