rails_pulse 0.1.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +638 -0
- data/Rakefile +207 -0
- data/app/assets/images/rails_pulse/dashboard.png +0 -0
- data/app/assets/images/rails_pulse/menu.svg +1 -0
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/images/rails_pulse/request.png +0 -0
- data/app/assets/images/rails_pulse/routes.png +0 -0
- data/app/assets/stylesheets/rails_pulse/application.css +102 -0
- data/app/assets/stylesheets/rails_pulse/components/alert.css +24 -0
- data/app/assets/stylesheets/rails_pulse/components/badge.css +58 -0
- data/app/assets/stylesheets/rails_pulse/components/base.css +79 -0
- data/app/assets/stylesheets/rails_pulse/components/breadcrumb.css +31 -0
- data/app/assets/stylesheets/rails_pulse/components/button.css +99 -0
- data/app/assets/stylesheets/rails_pulse/components/card.css +19 -0
- data/app/assets/stylesheets/rails_pulse/components/chart.css +18 -0
- data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +86 -0
- data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +9 -0
- data/app/assets/stylesheets/rails_pulse/components/dialog.css +56 -0
- data/app/assets/stylesheets/rails_pulse/components/flash.css +47 -0
- data/app/assets/stylesheets/rails_pulse/components/input.css +80 -0
- data/app/assets/stylesheets/rails_pulse/components/layouts.css +63 -0
- data/app/assets/stylesheets/rails_pulse/components/menu.css +43 -0
- data/app/assets/stylesheets/rails_pulse/components/popover.css +36 -0
- data/app/assets/stylesheets/rails_pulse/components/prose.css +144 -0
- data/app/assets/stylesheets/rails_pulse/components/row.css +24 -0
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +79 -0
- data/app/assets/stylesheets/rails_pulse/components/skeleton.css +5 -0
- data/app/assets/stylesheets/rails_pulse/components/table.css +37 -0
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +36 -0
- data/app/controllers/concerns/chart_table_concern.rb +82 -0
- data/app/controllers/concerns/response_range_concern.rb +24 -0
- data/app/controllers/concerns/time_range_concern.rb +67 -0
- data/app/controllers/concerns/zoom_range_concern.rb +40 -0
- data/app/controllers/rails_pulse/application_controller.rb +67 -0
- data/app/controllers/rails_pulse/assets_controller.rb +33 -0
- data/app/controllers/rails_pulse/caches_controller.rb +115 -0
- data/app/controllers/rails_pulse/csp_test_controller.rb +57 -0
- data/app/controllers/rails_pulse/dashboard_controller.rb +6 -0
- data/app/controllers/rails_pulse/operations_controller.rb +219 -0
- data/app/controllers/rails_pulse/queries_controller.rb +121 -0
- data/app/controllers/rails_pulse/requests_controller.rb +69 -0
- data/app/controllers/rails_pulse/routes_controller.rb +99 -0
- data/app/helpers/rails_pulse/application_helper.rb +111 -0
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +62 -0
- data/app/helpers/rails_pulse/cached_component_helper.rb +73 -0
- data/app/helpers/rails_pulse/chart_formatters.rb +43 -0
- data/app/helpers/rails_pulse/chart_helper.rb +140 -0
- data/app/helpers/rails_pulse/formatting_helper.rb +29 -0
- data/app/helpers/rails_pulse/status_helper.rb +279 -0
- data/app/helpers/rails_pulse/table_helper.rb +54 -0
- data/app/javascript/rails_pulse/application.js +119 -0
- data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +20 -0
- data/app/javascript/rails_pulse/controllers/context_menu_controller.js +16 -0
- data/app/javascript/rails_pulse/controllers/dialog_controller.js +21 -0
- data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +67 -0
- data/app/javascript/rails_pulse/controllers/form_controller.js +39 -0
- data/app/javascript/rails_pulse/controllers/icon_controller.js +170 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +230 -0
- data/app/javascript/rails_pulse/controllers/menu_controller.js +60 -0
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +69 -0
- data/app/javascript/rails_pulse/controllers/popover_controller.js +91 -0
- data/app/javascript/rails_pulse/controllers/timezone_controller.js +106 -0
- data/app/javascript/rails_pulse/theme.js +416 -0
- data/app/jobs/rails_pulse/application_job.rb +4 -0
- data/app/jobs/rails_pulse/cleanup_job.rb +21 -0
- data/app/mailers/rails_pulse/application_mailer.rb +6 -0
- data/app/models/rails_pulse/application_record.rb +7 -0
- data/app/models/rails_pulse/component_cache_key.rb +33 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +27 -0
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +37 -0
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +59 -0
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +45 -0
- data/app/models/rails_pulse/operation.rb +87 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +52 -0
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +57 -0
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +71 -0
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +112 -0
- data/app/models/rails_pulse/query.rb +58 -0
- data/app/models/rails_pulse/request.rb +64 -0
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +99 -0
- data/app/models/rails_pulse/requests/charts/operations_chart.rb +35 -0
- data/app/models/rails_pulse/route.rb +77 -0
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +54 -0
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +73 -0
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +73 -0
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +59 -0
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +115 -0
- data/app/models/rails_pulse/routes/tables/index.rb +63 -0
- data/app/services/rails_pulse/sql_query_normalizer.rb +124 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +19 -0
- data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +44 -0
- data/app/views/layouts/rails_pulse/application.html.erb +72 -0
- data/app/views/rails_pulse/caches/show.html.erb +9 -0
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +12 -0
- data/app/views/rails_pulse/components/_code_panel.html.erb +12 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +55 -0
- data/app/views/rails_pulse/components/_metric_row.html.erb +9 -0
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +241 -0
- data/app/views/rails_pulse/components/_panel.html.erb +56 -0
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +15 -0
- data/app/views/rails_pulse/components/_table.html.erb +50 -0
- data/app/views/rails_pulse/components/_table_head.html.erb +20 -0
- data/app/views/rails_pulse/components/_table_pagination.html.erb +45 -0
- data/app/views/rails_pulse/components/_time_period.html.erb +16 -0
- data/app/views/rails_pulse/csp_test/show.html.erb +207 -0
- data/app/views/rails_pulse/dashboard/charts/_bar_chart.html.erb +1 -0
- data/app/views/rails_pulse/dashboard/index.html.erb +64 -0
- data/app/views/rails_pulse/dashboard/tables/_routes_table.html.erb +32 -0
- data/app/views/rails_pulse/dashboard/tables/_standard_table.html.erb +1 -0
- data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +43 -0
- data/app/views/rails_pulse/operations/_operation_analysis_database.html.erb +12 -0
- data/app/views/rails_pulse/operations/_operation_analysis_generic.html.erb +15 -0
- data/app/views/rails_pulse/operations/_operation_analysis_other.html.erb +69 -0
- data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +39 -0
- data/app/views/rails_pulse/operations/show.html.erb +79 -0
- data/app/views/rails_pulse/queries/_show_table.html.erb +19 -0
- data/app/views/rails_pulse/queries/_table.html.erb +31 -0
- data/app/views/rails_pulse/queries/index.html.erb +64 -0
- data/app/views/rails_pulse/queries/show.html.erb +86 -0
- data/app/views/rails_pulse/requests/_operations.html.erb +85 -0
- data/app/views/rails_pulse/requests/_table.html.erb +31 -0
- data/app/views/rails_pulse/requests/index.html.erb +64 -0
- data/app/views/rails_pulse/requests/show.html.erb +44 -0
- data/app/views/rails_pulse/routes/_table.html.erb +29 -0
- data/app/views/rails_pulse/routes/index.html.erb +65 -0
- data/app/views/rails_pulse/routes/show.html.erb +67 -0
- data/app/views/rails_pulse/skeletons/_chart.html.erb +3 -0
- data/app/views/rails_pulse/skeletons/_metric_card.html.erb +20 -0
- data/app/views/rails_pulse/skeletons/_panel.html.erb +19 -0
- data/app/views/rails_pulse/skeletons/_table.html.erb +8 -0
- data/config/importmap.rb +12 -0
- data/config/initializers/rails_charts_csp_patch.rb +83 -0
- data/config/initializers/rails_pulse.rb +198 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20250227235904_create_routes.rb +12 -0
- data/db/migrate/20250227235915_create_requests.rb +19 -0
- data/db/migrate/20250228000000_create_queries.rb +14 -0
- data/db/migrate/20250228000056_create_operations.rb +24 -0
- data/lib/generators/rails_pulse/install_generator.rb +17 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +198 -0
- data/lib/rails_pulse/cleanup_service.rb +212 -0
- data/lib/rails_pulse/configuration.rb +176 -0
- data/lib/rails_pulse/engine.rb +88 -0
- data/lib/rails_pulse/middleware/asset_server.rb +84 -0
- data/lib/rails_pulse/middleware/request_collector.rb +120 -0
- data/lib/rails_pulse/migration.rb +29 -0
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +280 -0
- data/lib/rails_pulse/version.rb +3 -0
- data/lib/rails_pulse.rb +38 -0
- data/lib/tasks/rails_pulse_tasks.rake +138 -0
- data/public/rails-pulse-assets/csp-test.js +110 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +89 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +13 -0
- data/public/rails-pulse-assets/rails-pulse.css +1 -0
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -0
- data/public/rails-pulse-assets/rails-pulse.js +183 -0
- data/public/rails-pulse-assets/rails-pulse.js.map +7 -0
- metadata +339 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = [ "item" ]
|
5
|
+
static values = { index: Number }
|
6
|
+
|
7
|
+
#observer
|
8
|
+
|
9
|
+
initialize() {
|
10
|
+
this.#observer = new IntersectionObserver(this.#reset.bind(this))
|
11
|
+
}
|
12
|
+
|
13
|
+
connect() {
|
14
|
+
this.#observer.observe(this.element)
|
15
|
+
}
|
16
|
+
|
17
|
+
disconnect() {
|
18
|
+
this.#observer.disconnect()
|
19
|
+
}
|
20
|
+
|
21
|
+
prev() {
|
22
|
+
if (this.indexValue > 0) {
|
23
|
+
this.indexValue--
|
24
|
+
this.#update()
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
next() {
|
29
|
+
if (this.indexValue < this.#lastIndex) {
|
30
|
+
this.indexValue++
|
31
|
+
this.#update()
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
#reset([ entry ]) {
|
36
|
+
if (entry.isIntersecting) {
|
37
|
+
this.indexValue = 0
|
38
|
+
this.#update()
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
#update() {
|
43
|
+
this.#updateTabstops()
|
44
|
+
this.#focusCurrentItem()
|
45
|
+
}
|
46
|
+
|
47
|
+
#updateTabstops() {
|
48
|
+
this.itemTargets.forEach((it, index) => {
|
49
|
+
it.tabIndex = index === this.indexValue ? 0 : -1
|
50
|
+
})
|
51
|
+
}
|
52
|
+
|
53
|
+
#focusCurrentItem() {
|
54
|
+
this.itemTargets[this.indexValue].focus()
|
55
|
+
}
|
56
|
+
|
57
|
+
get #lastIndex() {
|
58
|
+
return this.itemTargets.length -1
|
59
|
+
}
|
60
|
+
}
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["limit"]
|
5
|
+
static values = {
|
6
|
+
storageKey: { type: String, default: "rails_pulse_pagination_limit" },
|
7
|
+
url: String
|
8
|
+
}
|
9
|
+
|
10
|
+
connect() {
|
11
|
+
this.restorePaginationLimit()
|
12
|
+
}
|
13
|
+
|
14
|
+
// Update pagination limit via AJAX and reload the page to reflect changes
|
15
|
+
async updateLimit() {
|
16
|
+
const limit = this.limitTarget.value
|
17
|
+
|
18
|
+
// Save to session storage
|
19
|
+
sessionStorage.setItem(this.storageKeyValue, limit)
|
20
|
+
|
21
|
+
try {
|
22
|
+
// Send AJAX request to update server session using Rails.ajax
|
23
|
+
const response = await fetch(this.urlValue, {
|
24
|
+
method: 'PATCH',
|
25
|
+
headers: {
|
26
|
+
'Content-Type': 'application/json',
|
27
|
+
'X-CSRF-Token': this.getCSRFToken()
|
28
|
+
},
|
29
|
+
body: JSON.stringify({ limit: limit })
|
30
|
+
})
|
31
|
+
|
32
|
+
if (response.ok) {
|
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)
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
// Get CSRF token from meta tag
|
45
|
+
getCSRFToken() {
|
46
|
+
const token = document.querySelector('meta[name="csrf-token"]')
|
47
|
+
return token ? token.getAttribute('content') : ''
|
48
|
+
}
|
49
|
+
|
50
|
+
// Save the pagination limit to session storage when it changes
|
51
|
+
savePaginationLimit() {
|
52
|
+
const limit = this.limitTarget.value
|
53
|
+
sessionStorage.setItem(this.storageKeyValue, limit)
|
54
|
+
}
|
55
|
+
|
56
|
+
// Restore the pagination limit from session storage on page load
|
57
|
+
restorePaginationLimit() {
|
58
|
+
const savedLimit = sessionStorage.getItem(this.storageKeyValue)
|
59
|
+
if (savedLimit && this.limitTarget) {
|
60
|
+
// Only set if the current value is different (prevents unnecessary DOM updates)
|
61
|
+
if (this.limitTarget.value !== savedLimit) {
|
62
|
+
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 }))
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { computePosition, flip, shift, offset, autoUpdate } from "@floating-ui/dom"
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = [ "button", "menu" ]
|
6
|
+
static values = { placement: { type: String, default: "bottom" } }
|
7
|
+
|
8
|
+
#showTimer = null
|
9
|
+
#hideTimer = null
|
10
|
+
|
11
|
+
initialize() {
|
12
|
+
this.orient = this.orient.bind(this)
|
13
|
+
}
|
14
|
+
|
15
|
+
connect() {
|
16
|
+
this.cleanup = autoUpdate(this.buttonTarget, this.menuTarget, this.orient)
|
17
|
+
}
|
18
|
+
|
19
|
+
disconnect() {
|
20
|
+
this.cleanup()
|
21
|
+
}
|
22
|
+
|
23
|
+
show(event) {
|
24
|
+
if (event) event.preventDefault()
|
25
|
+
this.menuTarget.showPopover({ source: this.buttonTarget })
|
26
|
+
// Explicitly call orient after showing to ensure positioning
|
27
|
+
this.orient()
|
28
|
+
this.loadOperationDetailsIfNeeded()
|
29
|
+
}
|
30
|
+
|
31
|
+
hide() {
|
32
|
+
this.menuTarget.hidePopover()
|
33
|
+
}
|
34
|
+
|
35
|
+
toggle(event) {
|
36
|
+
event.preventDefault()
|
37
|
+
this.menuTarget.togglePopover({ source: this.buttonTarget })
|
38
|
+
// Explicitly call orient after toggling to ensure positioning
|
39
|
+
this.orient()
|
40
|
+
this.loadOperationDetailsIfNeeded()
|
41
|
+
}
|
42
|
+
|
43
|
+
debouncedShow() {
|
44
|
+
clearTimeout(this.#hideTimer)
|
45
|
+
this.#showTimer = setTimeout(() => this.show(), 700)
|
46
|
+
}
|
47
|
+
|
48
|
+
debouncedHide() {
|
49
|
+
clearTimeout(this.#showTimer)
|
50
|
+
this.#hideTimer = setTimeout(() => this.hide(), 300)
|
51
|
+
}
|
52
|
+
|
53
|
+
orient() {
|
54
|
+
computePosition(this.buttonTarget, this.menuTarget, this.#options).then(({x, y}) => {
|
55
|
+
// Use CSS custom properties for CSP compliance
|
56
|
+
this.menuTarget.style.setProperty('--popover-x', `${x}px`)
|
57
|
+
this.menuTarget.style.setProperty('--popover-y', `${y}px`)
|
58
|
+
// Add class to apply the positioning
|
59
|
+
this.menuTarget.classList.add('positioned')
|
60
|
+
})
|
61
|
+
}
|
62
|
+
|
63
|
+
loadOperationDetailsIfNeeded() {
|
64
|
+
// Check if this popover has operation details to load
|
65
|
+
const operationUrl = this.menuTarget.dataset.operationUrl
|
66
|
+
if (!operationUrl) return
|
67
|
+
|
68
|
+
// Find the turbo frame inside the popover
|
69
|
+
const turboFrame = this.menuTarget.querySelector('turbo-frame')
|
70
|
+
if (!turboFrame) return
|
71
|
+
|
72
|
+
// Only load if not already loaded (check if still shows loading content)
|
73
|
+
// Use CSP-safe method to check for loading content
|
74
|
+
const hasLoadingContent = this.hasLoadingContent(turboFrame)
|
75
|
+
if (!hasLoadingContent) return
|
76
|
+
|
77
|
+
// Set the src attribute to trigger the turbo frame loading
|
78
|
+
turboFrame.src = operationUrl
|
79
|
+
}
|
80
|
+
|
81
|
+
// CSP-safe method to check for loading content
|
82
|
+
hasLoadingContent(element) {
|
83
|
+
// Use textContent instead of innerHTML to avoid CSP issues
|
84
|
+
const textContent = element.textContent || ''
|
85
|
+
return textContent.includes('Loading operation details')
|
86
|
+
}
|
87
|
+
|
88
|
+
get #options() {
|
89
|
+
return { placement: this.placementValue, middleware: [offset(4), flip(), shift({padding: 4})] }
|
90
|
+
}
|
91
|
+
}
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static values = { cachedAt: String, targetFrame: String }
|
5
|
+
|
6
|
+
connect() {
|
7
|
+
this.updateTimestamp()
|
8
|
+
this.setupObserver()
|
9
|
+
this.setupTurboFrameListener()
|
10
|
+
}
|
11
|
+
|
12
|
+
disconnect() {
|
13
|
+
if (this.observer) {
|
14
|
+
this.observer.disconnect()
|
15
|
+
}
|
16
|
+
if (this.frameObserver) {
|
17
|
+
this.frameObserver.disconnect()
|
18
|
+
}
|
19
|
+
if (this.documentObserver) {
|
20
|
+
this.documentObserver.disconnect()
|
21
|
+
}
|
22
|
+
if (this.turboFrameListener) {
|
23
|
+
document.removeEventListener('turbo:frame-load', this.turboFrameListener)
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
setupObserver() {
|
28
|
+
if (this.targetFrameValue) {
|
29
|
+
// Try immediately
|
30
|
+
this.updateTimestamp()
|
31
|
+
|
32
|
+
// Also observe for when the frame appears or changes
|
33
|
+
const observer = new MutationObserver(() => {
|
34
|
+
const targetFrame = document.getElementById(this.targetFrameValue)
|
35
|
+
if (targetFrame) {
|
36
|
+
this.updateTimestamp()
|
37
|
+
// Watch for attribute changes on the frame
|
38
|
+
if (this.frameObserver) {
|
39
|
+
this.frameObserver.disconnect()
|
40
|
+
}
|
41
|
+
this.frameObserver = new MutationObserver(() => {
|
42
|
+
this.updateTimestamp()
|
43
|
+
})
|
44
|
+
this.frameObserver.observe(targetFrame, {
|
45
|
+
attributes: true,
|
46
|
+
attributeFilter: ['data-cached-at'],
|
47
|
+
childList: true,
|
48
|
+
subtree: true
|
49
|
+
})
|
50
|
+
}
|
51
|
+
})
|
52
|
+
|
53
|
+
// Watch the whole document for the frame to appear
|
54
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
55
|
+
this.documentObserver = observer
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
cachedAtValueChanged() {
|
60
|
+
this.updateTimestamp()
|
61
|
+
}
|
62
|
+
|
63
|
+
setupTurboFrameListener() {
|
64
|
+
if (this.targetFrameValue) {
|
65
|
+
this.turboFrameListener = (event) => {
|
66
|
+
// Check if the loaded frame matches our target frame
|
67
|
+
if (event.target && event.target.id === this.targetFrameValue) {
|
68
|
+
// Update timestamp when our target frame loads
|
69
|
+
this.updateTimestamp()
|
70
|
+
}
|
71
|
+
}
|
72
|
+
document.addEventListener('turbo:frame-load', this.turboFrameListener)
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
updateTimestamp() {
|
77
|
+
let cachedAtValue = this.cachedAtValue
|
78
|
+
|
79
|
+
// If no direct cached value but we have a target frame, try to get it from there
|
80
|
+
if (!cachedAtValue && this.targetFrameValue) {
|
81
|
+
const targetFrame = document.getElementById(this.targetFrameValue)
|
82
|
+
if (targetFrame) {
|
83
|
+
cachedAtValue = targetFrame.dataset.cachedAt
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
if (cachedAtValue) {
|
88
|
+
try {
|
89
|
+
const date = new Date(cachedAtValue)
|
90
|
+
const localTimeString = date.toLocaleString('en-US', {
|
91
|
+
year: 'numeric',
|
92
|
+
month: 'long',
|
93
|
+
day: 'numeric',
|
94
|
+
hour: 'numeric',
|
95
|
+
minute: '2-digit',
|
96
|
+
hour12: true
|
97
|
+
})
|
98
|
+
this.element.title = `Last updated: ${localTimeString}`
|
99
|
+
} catch (e) {
|
100
|
+
this.element.title = 'Cache time unavailable'
|
101
|
+
}
|
102
|
+
} else {
|
103
|
+
this.element.title = 'Cache time unavailable'
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|