1import { cloneDeep, extend, isString } from 'lodash'; 2import { 3 dateMath, 4 dateTime, 5 getDefaultTimeRange, 6 isDateTime, 7 rangeUtil, 8 RawTimeRange, 9 TimeRange, 10 toUtc, 11} from '@grafana/data'; 12import { DashboardModel } from '../state/DashboardModel'; 13import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker'; 14import { config } from 'app/core/config'; 15import { getRefreshFromUrl } from '../utils/getRefreshFromUrl'; 16import { locationService } from '@grafana/runtime'; 17import { ShiftTimeEvent, ShiftTimeEventPayload, ZoomOutEvent } from '../../../types/events'; 18import { contextSrv, ContextSrv } from 'app/core/services/context_srv'; 19import appEvents from 'app/core/app_events'; 20 21export class TimeSrv { 22 time: any; 23 refreshTimer: any; 24 refresh: any; 25 previousAutoRefresh: any; 26 oldRefresh: string | null | undefined; 27 dashboard?: DashboardModel; 28 timeAtLoad: any; 29 private autoRefreshBlocked?: boolean; 30 31 constructor(private contextSrv: ContextSrv) { 32 // default time 33 this.time = getDefaultTimeRange().raw; 34 this.refreshDashboard = this.refreshDashboard.bind(this); 35 36 appEvents.subscribe(ZoomOutEvent, (e) => { 37 this.zoomOut(e.payload); 38 }); 39 40 appEvents.subscribe(ShiftTimeEvent, (e) => { 41 this.shiftTime(e.payload); 42 }); 43 44 document.addEventListener('visibilitychange', () => { 45 if (this.autoRefreshBlocked && document.visibilityState === 'visible') { 46 this.autoRefreshBlocked = false; 47 this.refreshDashboard(); 48 } 49 }); 50 } 51 52 init(dashboard: DashboardModel) { 53 this.dashboard = dashboard; 54 this.time = dashboard.time; 55 this.refresh = dashboard.refresh; 56 57 this.initTimeFromUrl(); 58 this.parseTime(); 59 60 // remember time at load so we can go back to it 61 this.timeAtLoad = cloneDeep(this.time); 62 63 const range = rangeUtil.convertRawToRange( 64 this.time, 65 this.dashboard?.getTimezone(), 66 this.dashboard?.fiscalYearStartMonth 67 ); 68 69 if (range.to.isBefore(range.from)) { 70 this.setTime( 71 { 72 from: range.raw.to, 73 to: range.raw.from, 74 }, 75 false 76 ); 77 } 78 79 if (this.refresh) { 80 this.setAutoRefresh(this.refresh); 81 } 82 } 83 84 getValidIntervals(intervals: string[]): string[] { 85 if (!this.contextSrv.minRefreshInterval) { 86 return intervals; 87 } 88 89 return intervals.filter((str) => str !== '').filter(this.contextSrv.isAllowedInterval); 90 } 91 92 private parseTime() { 93 // when absolute time is saved in json it is turned to a string 94 if (isString(this.time.from) && this.time.from.indexOf('Z') >= 0) { 95 this.time.from = dateTime(this.time.from).utc(); 96 } 97 if (isString(this.time.to) && this.time.to.indexOf('Z') >= 0) { 98 this.time.to = dateTime(this.time.to).utc(); 99 } 100 } 101 102 private parseUrlParam(value: any) { 103 if (value.indexOf('now') !== -1) { 104 return value; 105 } 106 if (value.length === 8) { 107 const utcValue = toUtc(value, 'YYYYMMDD'); 108 if (utcValue.isValid()) { 109 return utcValue; 110 } 111 } else if (value.length === 15) { 112 const utcValue = toUtc(value, 'YYYYMMDDTHHmmss'); 113 if (utcValue.isValid()) { 114 return utcValue; 115 } 116 } 117 118 if (!isNaN(value)) { 119 const epoch = parseInt(value, 10); 120 return toUtc(epoch); 121 } 122 123 return null; 124 } 125 126 private getTimeWindow(time: string, timeWindow: string) { 127 const valueTime = parseInt(time, 10); 128 let timeWindowMs; 129 130 if (timeWindow.match(/^\d+$/) && parseInt(timeWindow, 10)) { 131 // when time window specified in ms 132 timeWindowMs = parseInt(timeWindow, 10); 133 } else { 134 timeWindowMs = rangeUtil.intervalToMs(timeWindow); 135 } 136 137 return { 138 from: toUtc(valueTime - timeWindowMs / 2), 139 to: toUtc(valueTime + timeWindowMs / 2), 140 }; 141 } 142 143 private initTimeFromUrl() { 144 const params = locationService.getSearch(); 145 146 if (params.get('time') && params.get('time.window')) { 147 this.time = this.getTimeWindow(params.get('time')!, params.get('time.window')!); 148 } 149 150 if (params.get('from')) { 151 this.time.from = this.parseUrlParam(params.get('from')!) || this.time.from; 152 } 153 154 if (params.get('to')) { 155 this.time.to = this.parseUrlParam(params.get('to')!) || this.time.to; 156 } 157 158 // if absolute ignore refresh option saved to dashboard 159 if (params.get('to') && params.get('to')!.indexOf('now') === -1) { 160 this.refresh = false; 161 if (this.dashboard) { 162 this.dashboard.refresh = false; 163 } 164 } 165 166 let paramsJSON: Record<string, string> = {}; 167 params.forEach(function (value, key) { 168 paramsJSON[key] = value; 169 }); 170 171 // but if refresh explicitly set then use that 172 this.refresh = getRefreshFromUrl({ 173 params: paramsJSON, 174 currentRefresh: this.refresh, 175 refreshIntervals: Array.isArray(this.dashboard?.timepicker?.refresh_intervals) 176 ? this.dashboard?.timepicker?.refresh_intervals 177 : undefined, 178 isAllowedIntervalFn: this.contextSrv.isAllowedInterval, 179 minRefreshInterval: config.minRefreshInterval, 180 }); 181 } 182 183 updateTimeRangeFromUrl() { 184 const params = locationService.getSearch(); 185 186 if (params.get('left')) { 187 return; // explore handles this; 188 } 189 190 const urlRange = this.timeRangeForUrl(); 191 const from = params.get('from'); 192 const to = params.get('to'); 193 194 // check if url has time range 195 if (from && to) { 196 // is it different from what our current time range? 197 if (from !== urlRange.from || to !== urlRange.to) { 198 // issue update 199 this.initTimeFromUrl(); 200 this.setTime(this.time, true); 201 } 202 } else if (this.timeHasChangedSinceLoad()) { 203 this.setTime(this.timeAtLoad, true); 204 } 205 } 206 207 private timeHasChangedSinceLoad() { 208 return this.timeAtLoad && (this.timeAtLoad.from !== this.time.from || this.timeAtLoad.to !== this.time.to); 209 } 210 211 setAutoRefresh(interval: any) { 212 if (this.dashboard) { 213 this.dashboard.refresh = interval; 214 } 215 216 this.stopAutoRefresh(); 217 218 const currentUrlState = locationService.getSearchObject(); 219 220 if (!interval) { 221 // Clear URL state 222 if (currentUrlState.refresh) { 223 locationService.partial({ refresh: null }, true); 224 } 225 226 return; 227 } 228 229 const validInterval = this.contextSrv.getValidInterval(interval); 230 const intervalMs = rangeUtil.intervalToMs(validInterval); 231 232 this.refreshTimer = setTimeout(() => { 233 this.startNextRefreshTimer(intervalMs); 234 this.refreshDashboard(); 235 }, intervalMs); 236 237 const refresh = this.contextSrv.getValidInterval(interval); 238 239 if (currentUrlState.refresh !== refresh) { 240 locationService.partial({ refresh }, true); 241 } 242 } 243 244 refreshDashboard() { 245 this.dashboard?.timeRangeUpdated(this.timeRange()); 246 } 247 248 private startNextRefreshTimer(afterMs: number) { 249 this.refreshTimer = setTimeout(() => { 250 this.startNextRefreshTimer(afterMs); 251 if (this.contextSrv.isGrafanaVisible()) { 252 this.refreshDashboard(); 253 } else { 254 this.autoRefreshBlocked = true; 255 } 256 }, afterMs); 257 } 258 259 stopAutoRefresh() { 260 clearTimeout(this.refreshTimer); 261 } 262 263 // store dashboard refresh value and pause auto-refresh in some places 264 // i.e panel edit 265 pauseAutoRefresh() { 266 this.previousAutoRefresh = this.dashboard?.refresh; 267 this.setAutoRefresh(''); 268 } 269 270 // resume auto-refresh based on old dashboard refresh property 271 resumeAutoRefresh() { 272 this.setAutoRefresh(this.previousAutoRefresh); 273 } 274 275 setTime(time: RawTimeRange, fromRouteUpdate?: boolean) { 276 extend(this.time, time); 277 278 // disable refresh if zoom in or zoom out 279 if (isDateTime(time.to)) { 280 this.oldRefresh = this.dashboard?.refresh || this.oldRefresh; 281 this.setAutoRefresh(false); 282 } else if (this.oldRefresh && this.oldRefresh !== this.dashboard?.refresh) { 283 this.setAutoRefresh(this.oldRefresh); 284 this.oldRefresh = null; 285 } 286 287 // update url 288 if (fromRouteUpdate !== true) { 289 const urlRange = this.timeRangeForUrl(); 290 const urlParams = locationService.getSearchObject(); 291 292 if (urlParams.from === urlRange.from.toString() && urlParams.to === urlRange.to.toString()) { 293 return; 294 } 295 296 urlParams.from = urlRange.from.toString(); 297 urlParams.to = urlRange.to.toString(); 298 299 locationService.partial(urlParams); 300 } 301 302 this.refreshDashboard(); 303 } 304 305 timeRangeForUrl = () => { 306 const range = this.timeRange().raw; 307 308 if (isDateTime(range.from)) { 309 range.from = range.from.valueOf().toString(); 310 } 311 if (isDateTime(range.to)) { 312 range.to = range.to.valueOf().toString(); 313 } 314 315 return range; 316 }; 317 318 timeRange(): TimeRange { 319 // make copies if they are moment (do not want to return out internal moment, because they are mutable!) 320 const raw = { 321 from: isDateTime(this.time.from) ? dateTime(this.time.from) : this.time.from, 322 to: isDateTime(this.time.to) ? dateTime(this.time.to) : this.time.to, 323 }; 324 325 const timezone = this.dashboard ? this.dashboard.getTimezone() : undefined; 326 327 return { 328 from: dateMath.parse(raw.from, false, timezone, this.dashboard?.fiscalYearStartMonth)!, 329 to: dateMath.parse(raw.to, true, timezone, this.dashboard?.fiscalYearStartMonth)!, 330 raw: raw, 331 }; 332 } 333 334 zoomOut(factor: number) { 335 const range = this.timeRange(); 336 const { from, to } = getZoomedTimeRange(range, factor); 337 338 this.setTime({ from: toUtc(from), to: toUtc(to) }); 339 } 340 341 shiftTime(direction: ShiftTimeEventPayload) { 342 const range = this.timeRange(); 343 const { from, to } = getShiftedTimeRange(direction, range); 344 345 this.setTime({ 346 from: toUtc(from), 347 to: toUtc(to), 348 }); 349 } 350 351 // isRefreshOutsideThreshold function calculates the difference between last refresh and now 352 // if the difference is outside 5% of the current set time range then the function will return true 353 // if the difference is within 5% of the current set time range then the function will return false 354 // if the current time range is absolute (i.e. not using relative strings like now-5m) then the function will return false 355 isRefreshOutsideThreshold(lastRefresh: number, threshold = 0.05) { 356 const timeRange = this.timeRange(); 357 358 if (dateMath.isMathString(timeRange.raw.from)) { 359 const totalRange = timeRange.to.diff(timeRange.from); 360 const msSinceLastRefresh = Date.now() - lastRefresh; 361 const msThreshold = totalRange * threshold; 362 return msSinceLastRefresh >= msThreshold; 363 } 364 365 return false; 366 } 367} 368 369let singleton: TimeSrv | undefined; 370 371export function setTimeSrv(srv: TimeSrv) { 372 singleton = srv; 373} 374 375export function getTimeSrv(): TimeSrv { 376 if (!singleton) { 377 singleton = new TimeSrv(contextSrv); 378 } 379 380 return singleton; 381} 382