mirror of https://github.com/grafana/grafana
fix(browser history): fixes and enhancements to browser history, it now works properly again AND it can restore previous time ranges in dashboards, closes #7259
parent
60a2041065
commit
49fe74228b
@ -0,0 +1,110 @@ |
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; |
||||
|
||||
import helpers from 'test/specs/helpers'; |
||||
import _ from 'lodash'; |
||||
import TimeSrv from '../time_srv'; |
||||
import moment from 'moment'; |
||||
|
||||
describe('timeSrv', function() { |
||||
var ctx = new helpers.ServiceTestContext(); |
||||
var _dashboard: any = { |
||||
time: {from: 'now-6h', to: 'now'}, |
||||
}; |
||||
|
||||
beforeEach(angularMocks.module('grafana.core')); |
||||
beforeEach(angularMocks.module('grafana.services')); |
||||
beforeEach(ctx.createService('timeSrv')); |
||||
|
||||
beforeEach(function() { |
||||
ctx.service.init(_dashboard); |
||||
}); |
||||
|
||||
describe('timeRange', function() { |
||||
it('should return unparsed when parse is false', function() { |
||||
ctx.service.setTime({from: 'now', to: 'now-1h' }); |
||||
var time = ctx.service.timeRange(); |
||||
expect(time.raw.from).to.be('now'); |
||||
expect(time.raw.to).to.be('now-1h'); |
||||
}); |
||||
|
||||
it('should return parsed when parse is true', function() { |
||||
ctx.service.setTime({from: 'now', to: 'now-1h' }); |
||||
var time = ctx.service.timeRange(); |
||||
expect(moment.isMoment(time.from)).to.be(true); |
||||
expect(moment.isMoment(time.to)).to.be(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('init time from url', function() { |
||||
it('should handle relative times', function() { |
||||
ctx.$location.search({from: 'now-2d', to: 'now'}); |
||||
ctx.service.init(_dashboard); |
||||
var time = ctx.service.timeRange(); |
||||
expect(time.raw.from).to.be('now-2d'); |
||||
expect(time.raw.to).to.be('now'); |
||||
}); |
||||
|
||||
it('should handle formated dates', function() { |
||||
ctx.$location.search({from: '20140410T052010', to: '20140520T031022'}); |
||||
ctx.service.init(_dashboard); |
||||
var time = ctx.service.timeRange(true); |
||||
expect(time.from.valueOf()).to.equal(new Date("2014-04-10T05:20:10Z").getTime()); |
||||
expect(time.to.valueOf()).to.equal(new Date("2014-05-20T03:10:22Z").getTime()); |
||||
}); |
||||
|
||||
it('should handle formated dates without time', function() { |
||||
ctx.$location.search({from: '20140410', to: '20140520'}); |
||||
ctx.service.init(_dashboard); |
||||
var time = ctx.service.timeRange(true); |
||||
expect(time.from.valueOf()).to.equal(new Date("2014-04-10T00:00:00Z").getTime()); |
||||
expect(time.to.valueOf()).to.equal(new Date("2014-05-20T00:00:00Z").getTime()); |
||||
}); |
||||
|
||||
it('should handle epochs', function() { |
||||
ctx.$location.search({from: '1410337646373', to: '1410337665699'}); |
||||
ctx.service.init(_dashboard); |
||||
var time = ctx.service.timeRange(true); |
||||
expect(time.from.valueOf()).to.equal(1410337646373); |
||||
expect(time.to.valueOf()).to.equal(1410337665699); |
||||
}); |
||||
|
||||
it('should handle bad dates', function() { |
||||
ctx.$location.search({from: '20151126T00010%3C%2Fp%3E%3Cspan%20class', to: 'now'}); |
||||
_dashboard.time.from = 'now-6h'; |
||||
ctx.service.init(_dashboard); |
||||
expect(ctx.service.time.from).to.equal('now-6h'); |
||||
expect(ctx.service.time.to).to.equal('now'); |
||||
}); |
||||
}); |
||||
|
||||
describe('setTime', function() { |
||||
it('should return disable refresh if refresh is disabled for any range', function() { |
||||
_dashboard.refresh = false; |
||||
|
||||
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' }); |
||||
expect(_dashboard.refresh).to.be(false); |
||||
}); |
||||
|
||||
it('should restore refresh for absolute time range', function() { |
||||
_dashboard.refresh = '30s'; |
||||
|
||||
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' }); |
||||
expect(_dashboard.refresh).to.be('30s'); |
||||
}); |
||||
|
||||
it('should restore refresh after relative time range is set', function() { |
||||
_dashboard.refresh = '10s'; |
||||
ctx.service.setTime({from: moment([2011,1,1]), to: moment([2015,1,1])}); |
||||
expect(_dashboard.refresh).to.be(false); |
||||
ctx.service.setTime({from: '2011-01-01', to: 'now' }); |
||||
expect(_dashboard.refresh).to.be('10s'); |
||||
}); |
||||
|
||||
it('should keep refresh after relative time range is changed and now delay exists', function() { |
||||
_dashboard.refresh = '10s'; |
||||
ctx.service.setTime({from: 'now-1h', to: 'now-10s' }); |
||||
expect(_dashboard.refresh).to.be('10s'); |
||||
}); |
||||
}); |
||||
|
||||
}); |
||||
@ -1,170 +0,0 @@ |
||||
define([ |
||||
'angular', |
||||
'lodash', |
||||
'moment', |
||||
'app/core/config', |
||||
'app/core/utils/kbn', |
||||
'app/core/utils/datemath' |
||||
], function (angular, _, moment, config, kbn, dateMath) { |
||||
'use strict'; |
||||
|
||||
var module = angular.module('grafana.services'); |
||||
|
||||
module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer, contextSrv) { |
||||
var self = this; |
||||
|
||||
// default time
|
||||
this.time = {from: '6h', to: 'now'}; |
||||
|
||||
$rootScope.$on('zoom-out', function(e, factor) { self.zoomOut(factor); }); |
||||
|
||||
this.init = function(dashboard) { |
||||
timer.cancel_all(); |
||||
|
||||
this.dashboard = dashboard; |
||||
this.time = dashboard.time; |
||||
this.refresh = dashboard.refresh; |
||||
|
||||
this._initTimeFromUrl(); |
||||
this._parseTime(); |
||||
|
||||
if(this.refresh) { |
||||
this.setAutoRefresh(this.refresh); |
||||
} |
||||
}; |
||||
|
||||
this._parseTime = function() { |
||||
// when absolute time is saved in json it is turned to a string
|
||||
if (_.isString(this.time.from) && this.time.from.indexOf('Z') >= 0) { |
||||
this.time.from = moment(this.time.from).utc(); |
||||
} |
||||
if (_.isString(this.time.to) && this.time.to.indexOf('Z') >= 0) { |
||||
this.time.to = moment(this.time.to).utc(); |
||||
} |
||||
}; |
||||
|
||||
this._parseUrlParam = function(value) { |
||||
if (value.indexOf('now') !== -1) { |
||||
return value; |
||||
} |
||||
if (value.length === 8) { |
||||
return moment.utc(value, 'YYYYMMDD'); |
||||
} |
||||
if (value.length === 15) { |
||||
return moment.utc(value, 'YYYYMMDDTHHmmss'); |
||||
} |
||||
|
||||
if (!isNaN(value)) { |
||||
var epoch = parseInt(value); |
||||
return moment.utc(epoch); |
||||
} |
||||
|
||||
return null; |
||||
}; |
||||
|
||||
this._initTimeFromUrl = function() { |
||||
if ($routeParams.from) { |
||||
this.time.from = this._parseUrlParam($routeParams.from) || this.time.from; |
||||
} |
||||
if ($routeParams.to) { |
||||
this.time.to = this._parseUrlParam($routeParams.to) || this.time.to; |
||||
} |
||||
if ($routeParams.refresh) { |
||||
this.refresh = $routeParams.refresh || this.refresh; |
||||
} |
||||
}; |
||||
|
||||
this.setAutoRefresh = function (interval) { |
||||
this.dashboard.refresh = interval; |
||||
if (interval) { |
||||
var interval_ms = kbn.interval_to_ms(interval); |
||||
$timeout(function () { |
||||
self.start_scheduled_refresh(interval_ms); |
||||
self.refreshDashboard(); |
||||
}, interval_ms); |
||||
} else { |
||||
this.cancel_scheduled_refresh(); |
||||
} |
||||
}; |
||||
|
||||
this.refreshDashboard = function() { |
||||
$rootScope.$broadcast('refresh'); |
||||
}; |
||||
|
||||
this.start_scheduled_refresh = function (after_ms) { |
||||
self.cancel_scheduled_refresh(); |
||||
self.refresh_timer = timer.register($timeout(function () { |
||||
self.start_scheduled_refresh(after_ms); |
||||
if (contextSrv.isGrafanaVisible()) { |
||||
self.refreshDashboard(); |
||||
} |
||||
}, after_ms)); |
||||
}; |
||||
|
||||
this.cancel_scheduled_refresh = function () { |
||||
timer.cancel(this.refresh_timer); |
||||
}; |
||||
|
||||
this.setTime = function(time, enableRefresh) { |
||||
_.extend(this.time, time); |
||||
|
||||
// disable refresh if zoom in or zoom out
|
||||
if (!enableRefresh && moment.isMoment(time.to)) { |
||||
this.old_refresh = this.dashboard.refresh || this.old_refresh; |
||||
this.setAutoRefresh(false); |
||||
} |
||||
else if (this.old_refresh && this.old_refresh !== this.dashboard.refresh) { |
||||
this.setAutoRefresh(this.old_refresh); |
||||
this.old_refresh = null; |
||||
} |
||||
|
||||
$rootScope.appEvent('time-range-changed', this.time); |
||||
$timeout(this.refreshDashboard, 0); |
||||
}; |
||||
|
||||
this.timeRangeForUrl = function() { |
||||
var range = this.timeRange().raw; |
||||
|
||||
if (moment.isMoment(range.from)) { range.from = range.from.valueOf(); } |
||||
if (moment.isMoment(range.to)) { range.to = range.to.valueOf(); } |
||||
|
||||
return range; |
||||
}; |
||||
|
||||
this.timeRange = function() { |
||||
// make copies if they are moment (do not want to return out internal moment, because they are mutable!)
|
||||
var range = { |
||||
from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from, |
||||
to: moment.isMoment(this.time.to) ? moment(this.time.to) : this.time.to, |
||||
}; |
||||
|
||||
range = { |
||||
from: dateMath.parse(range.from, false), |
||||
to: dateMath.parse(range.to, true), |
||||
raw: range |
||||
}; |
||||
|
||||
return range; |
||||
}; |
||||
|
||||
this.zoomOut = function(factor) { |
||||
var range = this.timeRange(); |
||||
|
||||
var timespan = (range.to.valueOf() - range.from.valueOf()); |
||||
var center = range.to.valueOf() - timespan/2; |
||||
|
||||
var to = (center + (timespan*factor)/2); |
||||
var from = (center - (timespan*factor)/2); |
||||
|
||||
if (to > Date.now() && range.to <= Date.now()) { |
||||
var offset = to - Date.now(); |
||||
from = from - offset; |
||||
to = Date.now(); |
||||
} |
||||
|
||||
this.setTime({from: moment.utc(from), to: moment.utc(to) }); |
||||
}; |
||||
|
||||
}); |
||||
|
||||
}); |
||||
@ -0,0 +1,204 @@ |
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config'; |
||||
import angular from 'angular'; |
||||
import moment from 'moment'; |
||||
import _ from 'lodash'; |
||||
import coreModule from 'app/core/core_module'; |
||||
import kbn from 'app/core/utils/kbn'; |
||||
import * as dateMath from 'app/core/utils/datemath'; |
||||
|
||||
class TimeSrv { |
||||
time: any; |
||||
refreshTimer: any; |
||||
refresh: boolean; |
||||
oldRefresh: boolean; |
||||
dashboard: any; |
||||
timeAtLoad: any; |
||||
|
||||
/** @ngInject **/ |
||||
constructor(private $rootScope, private $timeout, private $location, private timer, private contextSrv) { |
||||
// default time
|
||||
this.time = {from: '6h', to: 'now'}; |
||||
|
||||
$rootScope.$on('zoom-out', this.zoomOut.bind(this)); |
||||
$rootScope.$on('$routeUpdate', this.routeUpdated.bind(this)); |
||||
} |
||||
|
||||
init(dashboard) { |
||||
this.timer.cancelAll(); |
||||
|
||||
this.dashboard = dashboard; |
||||
this.time = dashboard.time; |
||||
this.refresh = dashboard.refresh; |
||||
|
||||
this.initTimeFromUrl(); |
||||
this.parseTime(); |
||||
|
||||
// remember time at load so we can go back to it
|
||||
this.timeAtLoad = _.cloneDeep(this.time); |
||||
|
||||
if (this.refresh) { |
||||
this.setAutoRefresh(this.refresh); |
||||
} |
||||
} |
||||
|
||||
private parseTime() { |
||||
// when absolute time is saved in json it is turned to a string
|
||||
if (_.isString(this.time.from) && this.time.from.indexOf('Z') >= 0) { |
||||
this.time.from = moment(this.time.from).utc(); |
||||
} |
||||
if (_.isString(this.time.to) && this.time.to.indexOf('Z') >= 0) { |
||||
this.time.to = moment(this.time.to).utc(); |
||||
} |
||||
}; |
||||
|
||||
private parseUrlParam(value) { |
||||
if (value.indexOf('now') !== -1) { |
||||
return value; |
||||
} |
||||
if (value.length === 8) { |
||||
return moment.utc(value, 'YYYYMMDD'); |
||||
} |
||||
if (value.length === 15) { |
||||
return moment.utc(value, 'YYYYMMDDTHHmmss'); |
||||
} |
||||
|
||||
if (!isNaN(value)) { |
||||
var epoch = parseInt(value); |
||||
return moment.utc(epoch); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
private initTimeFromUrl() { |
||||
var params = this.$location.search(); |
||||
if (params.from) { |
||||
this.time.from = this.parseUrlParam(params.from) || this.time.from; |
||||
} |
||||
if (params.to) { |
||||
this.time.to = this.parseUrlParam(params.to) || this.time.to; |
||||
} |
||||
if (params.refresh) { |
||||
this.refresh = params.refresh || this.refresh; |
||||
} |
||||
}; |
||||
|
||||
private routeUpdated() { |
||||
var params = this.$location.search(); |
||||
var urlRange = this.timeRangeForUrl(); |
||||
// check if url has time range
|
||||
if (params.from && params.to) { |
||||
// is it different from what our current time range?
|
||||
if (params.from !== urlRange.from || params.to !== urlRange.to) { |
||||
// issue update
|
||||
this.initTimeFromUrl(); |
||||
this.setTime(this.time, true); |
||||
} |
||||
} else { |
||||
this.setTime(this.timeAtLoad, true); |
||||
} |
||||
} |
||||
|
||||
setAutoRefresh(interval) { |
||||
this.dashboard.refresh = interval; |
||||
if (interval) { |
||||
var intervalMs = kbn.interval_to_ms(interval); |
||||
|
||||
this.$timeout(() => { |
||||
this.startNextRefreshTimer(intervalMs); |
||||
this.refreshDashboard(); |
||||
}, intervalMs); |
||||
|
||||
} else { |
||||
this.cancelNextRefresh(); |
||||
} |
||||
} |
||||
|
||||
refreshDashboard() { |
||||
this.$rootScope.$broadcast('refresh'); |
||||
} |
||||
|
||||
private startNextRefreshTimer(afterMs) { |
||||
this.cancelNextRefresh(); |
||||
this.refreshTimer = this.timer.register(this.$timeout(() => { |
||||
this.startNextRefreshTimer(afterMs); |
||||
if (this.contextSrv.isGrafanaVisible()) { |
||||
this.refreshDashboard(); |
||||
} |
||||
}, afterMs)); |
||||
} |
||||
|
||||
private cancelNextRefresh() { |
||||
this.timer.cancel(this.refreshTimer); |
||||
}; |
||||
|
||||
setTime(time, fromRouteUpdate?) { |
||||
_.extend(this.time, time); |
||||
|
||||
// disable refresh if zoom in or zoom out
|
||||
if (moment.isMoment(time.to)) { |
||||
this.oldRefresh = this.dashboard.refresh || this.oldRefresh; |
||||
this.setAutoRefresh(false); |
||||
} else if (this.oldRefresh && this.oldRefresh !== this.dashboard.refresh) { |
||||
this.setAutoRefresh(this.oldRefresh); |
||||
this.oldRefresh = null; |
||||
} |
||||
|
||||
// update url
|
||||
if (fromRouteUpdate !== true) { |
||||
var urlRange = this.timeRangeForUrl(); |
||||
var urlParams = this.$location.search(); |
||||
urlParams.from = urlRange.from; |
||||
urlParams.to = urlRange.to; |
||||
this.$location.search(urlParams); |
||||
} |
||||
|
||||
this.$rootScope.appEvent('time-range-changed', this.time); |
||||
this.$timeout(this.refreshDashboard.bind(this), 0); |
||||
} |
||||
|
||||
timeRangeForUrl() { |
||||
var range = this.timeRange().raw; |
||||
|
||||
if (moment.isMoment(range.from)) { range.from = range.from.valueOf(); } |
||||
if (moment.isMoment(range.to)) { range.to = range.to.valueOf(); } |
||||
|
||||
return range; |
||||
} |
||||
|
||||
timeRange() { |
||||
// make copies if they are moment (do not want to return out internal moment, because they are mutable!)
|
||||
var raw = { |
||||
from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from, |
||||
to: moment.isMoment(this.time.to) ? moment(this.time.to) : this.time.to, |
||||
}; |
||||
|
||||
return { |
||||
from: dateMath.parse(raw.from, false), |
||||
to: dateMath.parse(raw.to, true), |
||||
raw: raw |
||||
}; |
||||
} |
||||
|
||||
zoomOut(e, factor) { |
||||
var range = this.timeRange(); |
||||
|
||||
var timespan = (range.to.valueOf() - range.from.valueOf()); |
||||
var center = range.to.valueOf() - timespan/2; |
||||
|
||||
var to = (center + (timespan*factor)/2); |
||||
var from = (center - (timespan*factor)/2); |
||||
|
||||
if (to > Date.now() && range.to <= Date.now()) { |
||||
var offset = to - Date.now(); |
||||
from = from - offset; |
||||
to = Date.now(); |
||||
} |
||||
|
||||
this.setTime({from: moment.utc(from), to: moment.utc(to)}); |
||||
} |
||||
} |
||||
|
||||
coreModule.service('timeSrv', TimeSrv); |
||||
@ -1,120 +0,0 @@ |
||||
define([ |
||||
'test/mocks/dashboard-mock', |
||||
'test/specs/helpers', |
||||
'lodash', |
||||
'moment', |
||||
'app/core/services/timer', |
||||
'app/features/dashboard/timeSrv' |
||||
], function(dashboardMock, helpers, _, moment) { |
||||
'use strict'; |
||||
|
||||
describe('timeSrv', function() { |
||||
var ctx = new helpers.ServiceTestContext(); |
||||
var _dashboard; |
||||
|
||||
beforeEach(module('grafana.core')); |
||||
beforeEach(module('grafana.services')); |
||||
beforeEach(ctx.providePhase(['$routeParams'])); |
||||
beforeEach(ctx.createService('timeSrv')); |
||||
|
||||
beforeEach(function() { |
||||
_dashboard = dashboardMock.create(); |
||||
ctx.service.init(_dashboard); |
||||
}); |
||||
|
||||
describe('timeRange', function() { |
||||
it('should return unparsed when parse is false', function() { |
||||
ctx.service.setTime({from: 'now', to: 'now-1h' }); |
||||
var time = ctx.service.timeRange(); |
||||
expect(time.raw.from).to.be('now'); |
||||
expect(time.raw.to).to.be('now-1h'); |
||||
}); |
||||
|
||||
it('should return parsed when parse is true', function() { |
||||
ctx.service.setTime({from: 'now', to: 'now-1h' }); |
||||
var time = ctx.service.timeRange(); |
||||
expect(moment.isMoment(time.from)).to.be(true); |
||||
expect(moment.isMoment(time.to)).to.be(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('init time from url', function() { |
||||
it('should handle relative times', function() { |
||||
ctx.$routeParams.from = 'now-2d'; |
||||
ctx.$routeParams.to = 'now'; |
||||
ctx.service.init(_dashboard); |
||||
var time = ctx.service.timeRange(); |
||||
expect(time.raw.from).to.be('now-2d'); |
||||
expect(time.raw.to).to.be('now'); |
||||
}); |
||||
|
||||
it('should handle formated dates', function() { |
||||
ctx.$routeParams.from = '20140410T052010'; |
||||
ctx.$routeParams.to = '20140520T031022'; |
||||
ctx.service.init(_dashboard); |
||||
var time = ctx.service.timeRange(true); |
||||
expect(time.from.valueOf()).to.equal(new Date("2014-04-10T05:20:10Z").getTime()); |
||||
expect(time.to.valueOf()).to.equal(new Date("2014-05-20T03:10:22Z").getTime()); |
||||
}); |
||||
|
||||
it('should handle formated dates without time', function() { |
||||
ctx.$routeParams.from = '20140410'; |
||||
ctx.$routeParams.to = '20140520'; |
||||
ctx.service.init(_dashboard); |
||||
var time = ctx.service.timeRange(true); |
||||
expect(time.from.valueOf()).to.equal(new Date("2014-04-10T00:00:00Z").getTime()); |
||||
expect(time.to.valueOf()).to.equal(new Date("2014-05-20T00:00:00Z").getTime()); |
||||
}); |
||||
|
||||
it('should handle epochs', function() { |
||||
ctx.$routeParams.from = '1410337646373'; |
||||
ctx.$routeParams.to = '1410337665699'; |
||||
ctx.service.init(_dashboard); |
||||
var time = ctx.service.timeRange(true); |
||||
expect(time.from.valueOf()).to.equal(1410337646373); |
||||
expect(time.to.valueOf()).to.equal(1410337665699); |
||||
}); |
||||
|
||||
it('should handle bad dates', function() { |
||||
ctx.$routeParams.from = '20151126T00010%3C%2Fp%3E%3Cspan%20class'; |
||||
ctx.$routeParams.to = 'now'; |
||||
_dashboard.time.from = 'now-6h'; |
||||
ctx.service.init(_dashboard); |
||||
expect(ctx.service.time.from).to.equal('now-6h'); |
||||
expect(ctx.service.time.to).to.equal('now'); |
||||
}); |
||||
}); |
||||
|
||||
describe('setTime', function() { |
||||
it('should return disable refresh if refresh is disabled for any range', function() { |
||||
_dashboard.refresh = false; |
||||
|
||||
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' }); |
||||
expect(_dashboard.refresh).to.be(false); |
||||
}); |
||||
|
||||
it('should restore refresh for absolute time range', function() { |
||||
_dashboard.refresh = '30s'; |
||||
|
||||
ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' }); |
||||
expect(_dashboard.refresh).to.be('30s'); |
||||
}); |
||||
|
||||
it('should restore refresh after relative time range is set', function() { |
||||
_dashboard.refresh = '10s'; |
||||
ctx.service.setTime({from: moment([2011,1,1]), to: moment([2015,1,1])}); |
||||
expect(_dashboard.refresh).to.be(false); |
||||
ctx.service.setTime({from: '2011-01-01', to: 'now' }); |
||||
expect(_dashboard.refresh).to.be('10s'); |
||||
}); |
||||
|
||||
it('should keep refresh after relative time range is changed and now delay exists', function() { |
||||
_dashboard.refresh = '10s'; |
||||
ctx.service.setTime({from: 'now-1h', to: 'now-10s' }); |
||||
expect(_dashboard.refresh).to.be('10s'); |
||||
}); |
||||
}); |
||||
|
||||
}); |
||||
|
||||
}); |
||||
Loading…
Reference in new issue