TimeSrv.ts 11 KB

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