diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bdeeed9af1..adc52560155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ * **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw) * **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist) * **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps) +* **Singlestat**: Fix "time of last point" shows local time when dashboard timezone set to UTC [#10338](https://github.com/grafana/grafana/issues/10338) * **Dashboards**: Version cleanup fails on old databases with many entries [#11278](https://github.com/grafana/grafana/issues/11278) * **Server**: Adjust permissions of unix socket [#11343](https://github.com/grafana/grafana/pull/11343), thx [@corny](https://github.com/corny) * **Shortcuts**: Add shortcut for duplicate panel [#11102](https://github.com/grafana/grafana/issues/11102) diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.jest.ts index 9fad1f30694..68945068043 100644 --- a/public/app/core/specs/kbn.jest.ts +++ b/public/app/core/specs/kbn.jest.ts @@ -101,38 +101,88 @@ describeValueFormat('d', 245, 100, 0, '35 week'); describeValueFormat('d', 2456, 10, 0, '6.73 year'); describe('date time formats', function() { + const epoch = 1505634997920; + const utcTime = moment.utc(epoch); + const browserTime = moment(epoch); + it('should format as iso date', function() { - var str = kbn.valueFormats.dateTimeAsIso(1505634997920, 1); - expect(str).toBe(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss')); + var expected = browserTime.format('YYYY-MM-DD HH:mm:ss'); + var actual = kbn.valueFormats.dateTimeAsIso(epoch); + expect(actual).toBe(expected); + }); + + it('should format as iso date (in UTC)', function() { + var expected = utcTime.format('YYYY-MM-DD HH:mm:ss'); + var actual = kbn.valueFormats.dateTimeAsIso(epoch, true); + expect(actual).toBe(expected); }); it('should format as iso date and skip date when today', function() { var now = moment(); - var str = kbn.valueFormats.dateTimeAsIso(now.valueOf(), 1); - expect(str).toBe(now.format('HH:mm:ss')); + var expected = now.format('HH:mm:ss'); + var actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), false); + expect(actual).toBe(expected); + }); + + it('should format as iso date (in UTC) and skip date when today', function() { + var now = moment.utc(); + var expected = now.format('HH:mm:ss'); + var actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), true); + expect(actual).toBe(expected); }); it('should format as US date', function() { - var str = kbn.valueFormats.dateTimeAsUS(1505634997920, 1); - expect(str).toBe(moment(1505634997920).format('MM/DD/YYYY h:mm:ss a')); + var expected = browserTime.format('MM/DD/YYYY h:mm:ss a'); + var actual = kbn.valueFormats.dateTimeAsUS(epoch, false); + expect(actual).toBe(expected); + }); + + it('should format as US date (in UTC)', function() { + var expected = utcTime.format('MM/DD/YYYY h:mm:ss a'); + var actual = kbn.valueFormats.dateTimeAsUS(epoch, true); + expect(actual).toBe(expected); }); it('should format as US date and skip date when today', function() { var now = moment(); - var str = kbn.valueFormats.dateTimeAsUS(now.valueOf(), 1); - expect(str).toBe(now.format('h:mm:ss a')); + var expected = now.format('h:mm:ss a'); + var actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), false); + expect(actual).toBe(expected); + }); + + it('should format as US date (in UTC) and skip date when today', function() { + var now = moment.utc(); + var expected = now.format('h:mm:ss a'); + var actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), true); + expect(actual).toBe(expected); }); it('should format as from now with days', function() { var daysAgo = moment().add(-7, 'd'); - var str = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), 1); - expect(str).toBe('7 days ago'); + var expected = '7 days ago'; + var actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); + expect(actual).toBe(expected); + }); + + it('should format as from now with days (in UTC)', function() { + var daysAgo = moment.utc().add(-7, 'd'); + var expected = '7 days ago'; + var actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); + expect(actual).toBe(expected); }); it('should format as from now with minutes', function() { var daysAgo = moment().add(-2, 'm'); - var str = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), 1); - expect(str).toBe('2 minutes ago'); + var expected = '2 minutes ago'; + var actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); + expect(actual).toBe(expected); + }); + + it('should format as from now with minutes (in UTC)', function() { + var daysAgo = moment.utc().add(-2, 'm'); + var expected = '2 minutes ago'; + var actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); + expect(actual).toBe(expected); }); }); diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index f4ee7af3383..f1c782846fc 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -816,8 +816,8 @@ kbn.valueFormats.timeticks = function(size, decimals, scaledDecimals) { return kbn.valueFormats.s(size / 100, decimals, scaledDecimals); }; -kbn.valueFormats.dateTimeAsIso = function(epoch) { - var time = moment(epoch); +kbn.valueFormats.dateTimeAsIso = function(epoch, isUtc) { + var time = isUtc ? moment.utc(epoch) : moment(epoch); if (moment().isSame(epoch, 'day')) { return time.format('HH:mm:ss'); @@ -825,8 +825,8 @@ kbn.valueFormats.dateTimeAsIso = function(epoch) { return time.format('YYYY-MM-DD HH:mm:ss'); }; -kbn.valueFormats.dateTimeAsUS = function(epoch) { - var time = moment(epoch); +kbn.valueFormats.dateTimeAsUS = function(epoch, isUtc) { + var time = isUtc ? moment.utc(epoch) : moment(epoch); if (moment().isSame(epoch, 'day')) { return time.format('h:mm:ss a'); @@ -834,8 +834,9 @@ kbn.valueFormats.dateTimeAsUS = function(epoch) { return time.format('MM/DD/YYYY h:mm:ss a'); }; -kbn.valueFormats.dateTimeFromNow = function(epoch) { - return moment(epoch).fromNow(); +kbn.valueFormats.dateTimeFromNow = function(epoch, isUtc) { + var time = isUtc ? moment.utc(epoch) : moment(epoch); + return time.fromNow(); }; ///// FORMAT MENU ///// diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 28d3f308d68..b1996d8ffc9 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -308,7 +308,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { let formatFunc = kbn.valueFormats[this.panel.format]; data.value = lastPoint[1]; data.valueRounded = data.value; - data.valueFormatted = formatFunc(data.value, 0, 0); + data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc()); } else { data.value = this.series[0].stats[this.panel.valueName]; data.flotpairs = this.series[0].flotpairs; diff --git a/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts b/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts index f80465052f9..217ec5ee04c 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts @@ -82,6 +82,19 @@ describe('SingleStatCtrl', function() { }); }); + singleStatScenario('showing last iso time instead of value (in UTC)', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.ctrl.panel.valueName = 'last_time'; + ctx.ctrl.panel.format = 'dateTimeAsIso'; + ctx.setIsUtc(true); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).to.be(moment.utc(1505634997920).format('YYYY-MM-DD HH:mm:ss')); + }); + }); + singleStatScenario('showing last us time instead of value', function(ctx) { ctx.setup(function() { ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; @@ -99,6 +112,19 @@ describe('SingleStatCtrl', function() { }); }); + singleStatScenario('showing last us time instead of value (in UTC)', function(ctx) { + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.ctrl.panel.valueName = 'last_time'; + ctx.ctrl.panel.format = 'dateTimeAsUS'; + ctx.setIsUtc(true); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).to.be(moment.utc(1505634997920).format('MM/DD/YYYY h:mm:ss a')); + }); + }); + singleStatScenario('showing last time from now instead of value', function(ctx) { beforeEach(() => { clock = sinon.useFakeTimers(epoch); @@ -124,6 +150,27 @@ describe('SingleStatCtrl', function() { }); }); + singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) { + beforeEach(() => { + clock = sinon.useFakeTimers(epoch); + }); + + ctx.setup(function() { + ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; + ctx.ctrl.panel.valueName = 'last_time'; + ctx.ctrl.panel.format = 'dateTimeFromNow'; + ctx.setIsUtc(true); + }); + + it('should set formatted value', function() { + expect(ctx.data.valueFormatted).to.be('2 days ago'); + }); + + afterEach(() => { + clock.restore(); + }); + }); + singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function( ctx ) { diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index ab7fbb5698b..276d9867ec4 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -23,6 +23,7 @@ export function ControllerTestContext() { }; }, }; + this.isUtc = false; this.providePhase = function(mocks) { return angularMocks.module(function($provide) { @@ -46,6 +47,10 @@ export function ControllerTestContext() { self.$q = $q; self.panel = new PanelModel({ type: 'test' }); self.dashboard = { meta: {} }; + self.isUtc = false; + self.dashboard.isTimezoneUtc = function() { + return self.isUtc; + }; $rootScope.appEvent = sinon.spy(); $rootScope.onAppEvent = sinon.spy(); @@ -93,6 +98,10 @@ export function ControllerTestContext() { }); }); }; + + this.setIsUtc = function(isUtc = false) { + self.isUtc = isUtc; + }; } export function ServiceTestContext() {