diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index 8015993634c8d..291208b2d9032 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -26,11 +26,10 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { memoize, padLeft, range } from 'lodash'; +import { padLeft, range } from 'lodash'; import moment from 'moment-timezone'; import React, { Component } from 'react'; import styled from 'styled-components'; -import { LegacyCoreStart } from 'src/core/public'; import { KibanaCoreContext } from '../../../../../../observability/public'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { KibanaLink } from '../../../shared/Links/KibanaLink'; @@ -39,12 +38,6 @@ import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; type ScheduleKey = keyof Schedule; -const getUserTimezone = memoize((core: LegacyCoreStart): string => { - return core.uiSettings.get('dateFormat:tz') === 'Browser' - ? moment.tz.guess() - : core.uiSettings.get('dateFormat:tz'); -}); - const SmallInput = styled.div` .euiFormRow { max-width: 85px; @@ -284,17 +277,13 @@ export class WatcherFlyout extends Component< return null; } - const core = this.context; - const userTimezoneSetting = getUserTimezone(core); const dailyTime = this.state.daily; const inputTime = `${dailyTime}Z`; // Add tz to make into UTC const inputFormat = 'HH:mmZ'; // Parse as 24 hour w. tz - const dailyTimeFormatted = moment(inputTime, inputFormat) - .tz(userTimezoneSetting) - .format('HH:mm'); // Format as 24h - const dailyTime12HourFormatted = moment(inputTime, inputFormat) - .tz(userTimezoneSetting) - .format('hh:mm A (z)'); // Format as 12h w. tz + const dailyTimeFormatted = moment(inputTime, inputFormat).format('HH:mm'); // Format as 24h + const dailyTime12HourFormatted = moment(inputTime, inputFormat).format( + 'hh:mm A (z)' + ); // Format as 12h w. tz // Generate UTC hours for Daily Report select field const intervalHours = range(24).map(i => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx index 73bba04043750..c9f6afa0b3611 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx @@ -17,17 +17,16 @@ describe('TimestampTooltip', () => { // mock Date.now mockNow(1570737000000); - // hardcode timezone to avoid timezone issues on CI - jest - .spyOn(moment.tz, 'guess') - .mockImplementation(() => 'Europe/Copenhagen'); + moment.tz.setDefault('Etc/GMT'); }); + afterAll(() => moment.tz.setDefault('')); + it('should render component with relative time in body and absolute time in tooltip', () => { expect(shallow()) .toMatchInlineSnapshot(` @@ -41,7 +40,7 @@ describe('TimestampTooltip', () => { shallow() .find('EuiToolTip') .prop('content') - ).toBe('Oct 10th 2019, 17:06:40.123 (+0200 CEST)'); + ).toBe('Oct 10th 2019, 15:06:40.123 (+0000 GMT)'); }); it('should format with precision in minutes', () => { @@ -49,7 +48,7 @@ describe('TimestampTooltip', () => { shallow() .find('EuiToolTip') .prop('content') - ).toBe('Oct 10th 2019, 17:06 (+0200 CEST)'); + ).toBe('Oct 10th 2019, 15:06 (+0000 GMT)'); }); it('should format with precision in days', () => { @@ -57,6 +56,6 @@ describe('TimestampTooltip', () => { shallow() .find('EuiToolTip') .prop('content') - ).toBe('Oct 10th 2019 (+0200 CEST)'); + ).toBe('Oct 10th 2019 (+0000 GMT)'); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx index 0a81c965f8add..1f7fcccdd56d0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx @@ -27,7 +27,7 @@ function getPreciseTime(precision: Props['precision']) { } export function TimestampTooltip({ time, precision = 'milliseconds' }: Props) { - const momentTime = moment.tz(time, moment.tz.guess()); + const momentTime = moment(time); const relativeTimeLabel = momentTime.fromNow(); const absoluteTimeLabel = momentTime.format( `MMM Do YYYY${getPreciseTime(precision)} (ZZ zz)` diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js index 5a1cdc185ac4f..dd95a0a3db12d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js @@ -17,11 +17,13 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import { last } from 'lodash'; import { rgba } from 'polished'; +import { scaleUtc } from 'd3-scale'; import StatusText from './StatusText'; import { SharedPlot } from './plotUtils'; import { i18n } from '@kbn/i18n'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { getTimezoneOffsetInMs } from './getTimezoneOffsetInMs'; // undefined values are converted by react-vis into NaN when stacking // see https://github.com/uber/react-vis/issues/1214 @@ -42,7 +44,7 @@ class StaticPlot extends PureComponent { { + const xDomain = this.props.plotValues.x.domain(); + + const time = value.getTime(); + + return scaleUtc() + .domain(xDomain) + .tickFormat()(new Date(time - getTimezoneOffsetInMs(time))); + }; + render() { - const { - width, - series, - tickFormatX, - tickFormatY, - plotValues, - noHits - } = this.props; - const { yTickValues } = plotValues; + const { width, series, tickFormatY, plotValues, noHits } = this.props; + const { xTickValues, yTickValues } = plotValues; // approximate number of x-axis ticks based on the width of the plot. There should by approx 1 tick per 100px // d3 will determine the exact number of ticks based on the selected range const xTickTotal = Math.floor(width / 100); + const tickFormatX = this.props.tickFormatX || this.tickFormatXTime; + return ( - + {noHits ? ( { + describe('when no default timezone is set', () => { + it('guesses the timezone', () => { + const guess = jest.fn(() => 'Etc/UTC'); + jest.spyOn(moment.tz, 'guess').mockImplementationOnce(guess); + + getTimezoneOffsetInMs(Date.now()); + + expect(guess).toHaveBeenCalled(); + }); + }); + + describe('when a default timezone is set', () => { + let originalTimezone: moment.MomentZone | null; + + beforeAll(() => { + // @ts-ignore moment types do not define defaultZone but it's there + originalTimezone = moment.defaultZone; + moment.tz.setDefault('America/Denver'); + }); + + afterAll(() => { + moment.tz.setDefault(originalTimezone ? originalTimezone.name : ''); + }); + + it('returns the time in milliseconds', () => { + expect(getTimezoneOffsetInMs(Date.now())).toEqual(21600000); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts new file mode 100644 index 0000000000000..178707bfd3c48 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment-timezone'; + +export function getTimezoneOffsetInMs(time: number) { + // @ts-ignore moment types don't define defaultZone but it's there + const zone = moment.defaultZone ? moment.defaultZone.name : moment.tz.guess(); + + // @ts-ignore + return moment.tz.zone(zone).parse(time) * 60000; +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js index 677fe85765304..7923ae25c22a3 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js @@ -12,6 +12,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { unit } from '../../../../style/variables'; +import { getTimezoneOffsetInMs } from './getTimezoneOffsetInMs'; const XY_HEIGHT = unit * 16; const XY_MARGIN = { @@ -64,15 +65,30 @@ export function getPlotValues( yMin = d3.min(flattenedCoordinates, d => d.y); } + const [xMinZone, xMaxZone] = [xMin, xMax].map(x => { + return x - getTimezoneOffsetInMs(x); + }); + const xScale = getXScale(xMin, xMax, width); const yScale = getYScale(yMin, yMax); const yMaxNice = yScale.domain()[1]; const yTickValues = [0, yMaxNice / 2, yMaxNice]; + const xTickValues = d3.time.scale + .utc() + .domain([xMinZone, xMaxZone]) + .range([0, width]) + .ticks() + .map(x => { + const time = x.getTime(); + return new Date(time + getTimezoneOffsetInMs(time)); + }); + return { x: xScale, y: yScale, + xTickValues, yTickValues, XY_MARGIN, XY_HEIGHT: height || XY_HEIGHT, @@ -90,7 +106,7 @@ export function SharedPlot({ plotValues, ...props }) { dontCheckIfEmpty height={height} margin={margin} - xType="time" + xType="time-utc" width={width} xDomain={plotValues.x.domain()} yDomain={plotValues.y.domain()} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts new file mode 100644 index 0000000000000..55bfb490e8588 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import * as plotUtils from './plotUtils'; + +describe('plotUtils', () => { + describe('getPlotValues', () => { + describe('with empty arguments', () => { + it('returns plotvalues', () => { + expect( + plotUtils.getPlotValues([], [], { height: 1, width: 1 }) + ).toMatchObject({ + XY_HEIGHT: 1, + XY_WIDTH: 1 + }); + }); + }); + + describe('when yMin is given', () => { + it('uses the yMin in the scale', () => { + expect( + plotUtils + .getPlotValues([], [], { height: 1, width: 1, yMin: 100 }) + .y.domain()[0] + ).toEqual(100); + }); + + describe('when yMin is "min"', () => { + it('uses minimum y from the series', () => { + expect( + plotUtils + .getPlotValues( + [{ data: { x: 0, y: 200 } }, { data: { x: 0, y: 300 } }], + [], + { + height: 1, + width: 1, + yMin: 'min' + } + ) + .y.domain()[0] + ).toEqual(200); + }); + }); + }); + + describe('when yMax given', () => { + it('uses yMax', () => { + expect( + plotUtils + .getPlotValues([], [], { + height: 1, + width: 1, + yMax: 500 + }) + .y.domain()[1] + ).toEqual(500); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js index c4c2f42027f63..7b9586634c7d0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -123,7 +123,8 @@ export class HistogramInner extends PureComponent { const xDomain = x.domain(); const yDomain = y.domain(); const yTickValues = [0, yDomain[1] / 2, yDomain[1]]; - const isTimeSeries = this.props.xType === 'time'; + const isTimeSeries = + this.props.xType === 'time' || this.props.xType === 'time-utc'; const shouldShowTooltip = hoveredBucket.x > 0 && (hoveredBucket.y > 0 || isTimeSeries);