Shows the selected time range duration in seconds/minutes next to the date picker
Size
7.0 KB
Version
1.0.1
Created
Nov 9, 2025
Updated
15 days ago
1// ==UserScript==
2// @name Grafana Time Range Duration Display
3// @description Shows the selected time range duration in seconds/minutes next to the date picker
4// @version 1.0.1
5// @match https://*.impala.app.cx498.coralogix.com/*
6// @icon https://impala.app.cx498.coralogix.com/grafana/public/img/fav32.png
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Grafana Time Range Duration Display extension loaded');
12
13 // Debounce function to avoid excessive updates
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // Format duration into human-readable format
27 function formatDuration(milliseconds) {
28 const seconds = Math.floor(milliseconds / 1000);
29 const minutes = Math.floor(seconds / 60);
30 const hours = Math.floor(minutes / 60);
31 const days = Math.floor(hours / 24);
32
33 if (days > 0) {
34 const remainingHours = hours % 24;
35 return `${days}d ${remainingHours}h (${seconds.toLocaleString()}s)`;
36 } else if (hours > 0) {
37 const remainingMinutes = minutes % 60;
38 return `${hours}h ${remainingMinutes}m (${seconds.toLocaleString()}s)`;
39 } else if (minutes > 0) {
40 const remainingSeconds = seconds % 60;
41 return `${minutes}m ${remainingSeconds}s (${seconds.toLocaleString()}s)`;
42 } else {
43 return `${seconds}s`;
44 }
45 }
46
47 // Extract time range from URL
48 function getTimeRangeFromURL() {
49 const urlParams = new URLSearchParams(window.location.search);
50 const from = urlParams.get('from');
51 const to = urlParams.get('to');
52
53 if (from && to) {
54 const fromMs = parseInt(from);
55 const toMs = parseInt(to);
56 const duration = toMs - fromMs;
57 return { from: fromMs, to: toMs, duration };
58 }
59 return null;
60 }
61
62 // Create and insert the duration display element
63 function createDurationDisplay() {
64 const timeRange = getTimeRangeFromURL();
65 if (!timeRange) {
66 console.log('No time range found in URL');
67 return;
68 }
69
70 // Find the time picker button
71 const timePickerButton = document.querySelector('button[data-testid="data-testid TimePicker Open Button"]');
72 if (!timePickerButton) {
73 console.log('Time picker button not found');
74 return;
75 }
76
77 // Remove existing duration display if present
78 const existingDisplay = document.getElementById('time-range-duration-display');
79 if (existingDisplay) {
80 existingDisplay.remove();
81 }
82
83 // Create the duration display element
84 const durationDisplay = document.createElement('div');
85 durationDisplay.id = 'time-range-duration-display';
86 durationDisplay.style.cssText = `
87 display: inline-flex;
88 align-items: center;
89 margin-left: 8px;
90 padding: 6px 12px;
91 background: rgba(36, 41, 46, 0.9);
92 border: 1px solid rgba(110, 159, 255, 0.4);
93 border-radius: 4px;
94 color: #6e9fff;
95 font-size: 13px;
96 font-weight: 500;
97 font-family: inherit;
98 white-space: nowrap;
99 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
100 `;
101
102 const formattedDuration = formatDuration(timeRange.duration);
103 durationDisplay.innerHTML = `
104 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
105 <circle cx="12" cy="12" r="10"></circle>
106 <polyline points="12 6 12 12 16 14"></polyline>
107 </svg>
108 <span style="color: #c7d0d9;">Duration:</span>
109 <span style="margin-left: 6px; color: #6e9fff; font-weight: 600;">${formattedDuration}</span>
110 `;
111
112 // Insert after the time picker button group
113 const buttonGroup = timePickerButton.closest('.button-group');
114 if (buttonGroup && buttonGroup.parentElement) {
115 buttonGroup.parentElement.insertBefore(durationDisplay, buttonGroup.nextSibling);
116 console.log('Duration display added:', formattedDuration);
117 }
118 }
119
120 // Initialize the extension
121 function init() {
122 console.log('Initializing Grafana Time Range Duration Display');
123
124 // Wait for the page to be fully loaded
125 if (document.readyState === 'loading') {
126 document.addEventListener('DOMContentLoaded', init);
127 return;
128 }
129
130 // Initial creation with a delay to ensure Grafana UI is ready
131 setTimeout(() => {
132 createDurationDisplay();
133 }, 1000);
134
135 // Watch for URL changes (Grafana is a SPA)
136 let lastUrl = window.location.href;
137 const urlObserver = debounce(() => {
138 const currentUrl = window.location.href;
139 if (currentUrl !== lastUrl) {
140 lastUrl = currentUrl;
141 console.log('URL changed, updating duration display');
142 setTimeout(createDurationDisplay, 500);
143 }
144 }, 300);
145
146 // Use MutationObserver to detect DOM changes
147 const observer = new MutationObserver(debounce(() => {
148 urlObserver();
149 // Also check if the time picker exists and duration display doesn't
150 const timePickerExists = document.querySelector('button[data-testid="data-testid TimePicker Open Button"]');
151 const durationExists = document.getElementById('time-range-duration-display');
152 if (timePickerExists && !durationExists) {
153 console.log('Time picker found but duration display missing, recreating');
154 createDurationDisplay();
155 }
156 }, 500));
157
158 observer.observe(document.body, {
159 childList: true,
160 subtree: true
161 });
162
163 // Listen for popstate events (browser back/forward)
164 window.addEventListener('popstate', () => {
165 console.log('Navigation detected via popstate');
166 setTimeout(createDurationDisplay, 500);
167 });
168
169 // Listen for pushstate/replacestate (SPA navigation)
170 const originalPushState = history.pushState;
171 const originalReplaceState = history.replaceState;
172
173 history.pushState = function() {
174 originalPushState.apply(this, arguments);
175 console.log('Navigation detected via pushState');
176 setTimeout(createDurationDisplay, 500);
177 };
178
179 history.replaceState = function() {
180 originalReplaceState.apply(this, arguments);
181 console.log('Navigation detected via replaceState');
182 setTimeout(createDurationDisplay, 500);
183 };
184 }
185
186 // Start the extension
187 init();
188})();