diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 2667d03ef8dd..daba164a4a00 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; +import { asRelativeDateTimeRange } from '../../../../utils/formatters'; interface IBucket { key: number; @@ -51,6 +52,9 @@ interface Props { title: React.ReactNode; } +const tooltipHeader = (bucket: FormattedBucket) => + asRelativeDateTimeRange(bucket.x0, bucket.x); + export function ErrorDistribution({ distribution, title }: Props) { const buckets = getFormattedBuckets( distribution.buckets, @@ -73,6 +77,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time" buckets={buckets} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index 2f06f1d52de6..a6c805815857 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -143,7 +143,7 @@ const ErrorGroupList: React.FC = props => { align: 'right', render: (value?: number) => value ? ( - + ) : ( NOT_AVAILABLE_LABEL ) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index f2524ef1c16f..13e7a5bfd894 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { fontSizes, truncate } from '../../../../style/variables'; -import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ManagedTable } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; @@ -80,7 +80,11 @@ export const SERVICE_COLUMNS = [ }), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx index 161d37114847..c660455e1eed 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx @@ -128,7 +128,7 @@ export function AgentConfigurationList({ ), sortable: true, render: (value: number) => ( - + ) }, { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index ca10b06c11cb..9116e02870a8 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -10,7 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { ITransactionGroup } from '../../../../server/lib/transaction_groups/transform'; import { fontSizes, truncate } from '../../../style/variables'; -import { asMillis } from '../../../utils/formatters'; +import { convertTo } from '../../../utils/formatters'; import { EmptyMessage } from '../../shared/EmptyMessage'; import { ImpactBar } from '../../shared/ImpactBar'; import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink'; @@ -66,7 +66,11 @@ const traceListColumns: Array> = [ }), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index fc86f4bb78af..c9e5175a1092 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -11,7 +11,7 @@ import React, { FunctionComponent, useCallback } from 'react'; import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { getTimeFormatter, timeUnit } from '../../../../utils/formatters'; +import { getDurationFormatter } from '../../../../utils/formatters'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; @@ -132,8 +132,7 @@ export const TransactionDistribution: FunctionComponent = ( ); const xMax = d3.max(buckets, d => d.x) || 0; - const timeFormatter = getTimeFormatter(xMax); - const unit = timeUnit(xMax); + const timeFormatter = getDurationFormatter(xMax); const bucketIndex = buckets.findIndex( bucket => @@ -187,18 +186,18 @@ export const TransactionDistribution: FunctionComponent = ( }); } }} - formatX={timeFormatter} + formatX={(time: number) => timeFormatter(time).formatted} formatYShort={formatYShort} formatYLong={formatYLong} verticalLineHover={(bucket: IChartPoint) => bucket.y > 0 && !bucket.sample } backgroundHover={(bucket: IChartPoint) => bucket.y > 0 && bucket.sample} - tooltipHeader={(bucket: IChartPoint) => - `${timeFormatter(bucket.x0, { - withUnit: false - })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` - } + tooltipHeader={(bucket: IChartPoint) => { + const xFormatted = timeFormatter(bucket.x); + const x0Formatted = timeFormatter(bucket.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + }} tooltipFooter={(bucket: IChartPoint) => !bucket.sample && i18n.translate( diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index a5e6eb622e8f..c64231a6ded8 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -12,7 +12,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; -import { asTime } from '../../../../../../utils/formatters'; +import { asDuration } from '../../../../../../utils/formatters'; import { ErrorCountBadge } from '../../ErrorCountBadge'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; @@ -133,7 +133,7 @@ const SpanActionToolTip: React.SFC = ({ function Duration({ item }: { item: IWaterfallItem }) { return ( - {asTime(item.duration)} + {asDuration(item.duration)} ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx index 062d103bfc44..3d75011f52f1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform'; import { fontFamilyCode, truncate } from '../../../../style/variables'; -import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ImpactBar } from '../../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -28,6 +28,12 @@ interface Props { isLoading: boolean; } +const toMilliseconds = (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted; + export function TransactionList({ items, isLoading }: Props) { const columns: Array> = useMemo( () => [ @@ -67,7 +73,7 @@ export function TransactionList({ items, isLoading }: Props) { ), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => toMilliseconds(time) }, { field: 'p95', @@ -79,7 +85,7 @@ export function TransactionList({ items, isLoading }: Props) { ), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => toMilliseconds(time) }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx index c76e62d987aa..a5a677296825 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip, EuiText } from '@elastic/eui'; import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent'; -import { asTime } from '../../../utils/formatters'; +import { asDuration } from '../../../utils/formatters'; interface Props { duration: number; @@ -29,7 +29,7 @@ const DurationSummaryItem = ({ return ( <> - {asTime(duration)} + {asDuration(duration)}   diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx index c4b750a360ef..ce6935d1858a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx @@ -8,9 +8,10 @@ import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { px, units } from '../../../../public/style/variables'; +import { Maybe } from '../../../../typings/common'; interface Props { - items: Array; + items: Array>; } // TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx new file mode 100644 index 000000000000..b4678b287dc1 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; +import moment from 'moment-timezone'; +import { TimestampTooltip } from '../index'; +import { mockNow } from '../../../../utils/testHelpers'; + +describe('TimestampTooltip', () => { + const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) + + beforeAll(() => { + // mock Date.now + mockNow(1570737000000); + + moment.tz.setDefault('America/Los_Angeles'); + }); + + afterAll(() => moment.tz.setDefault('')); + + it('should render component with relative time in body and absolute time in tooltip', () => { + expect(shallow()) + .toMatchInlineSnapshot(` + + 5 hours ago + + `); + }); + + it('should format with precision in milliseconds by default', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)'); + }); + + it('should format with precision in seconds', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06:40 (UTC-7)'); + }); + + it('should format with precision in minutes', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06 (UTC-7)'); + }); +}); 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 deleted file mode 100644 index a7149c760469..000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import React from 'react'; -import moment from 'moment-timezone'; -import { TimestampTooltip, asAbsoluteTime } from './index'; -import { mockNow } from '../../../utils/testHelpers'; - -describe('asAbsoluteTime', () => { - afterAll(() => moment.tz.setDefault('')); - - it('should add a leading plus for timezones with positive UTC offset', () => { - moment.tz.setDefault('Europe/Copenhagen'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 14:00 (UTC+2)' - ); - }); - - it('should add a leading minus for timezones with negative UTC offset', () => { - moment.tz.setDefault('America/Los_Angeles'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 05:00 (UTC-7)' - ); - }); - - it('should use default UTC offset formatting when offset contains minutes', () => { - moment.tz.setDefault('Canada/Newfoundland'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 09:30 (UTC-02:30)' - ); - }); - - it('should respect DST', () => { - moment.tz.setDefault('Europe/Copenhagen'); - const timeWithDST = 1559390400000; // Jun 1, 2019 - const timeWithoutDST = 1575201600000; // Dec 1, 2019 - - expect(asAbsoluteTime({ time: timeWithDST })).toBe( - 'Jun 1, 2019, 14:00:00.000 (UTC+2)' - ); - - expect(asAbsoluteTime({ time: timeWithoutDST })).toBe( - 'Dec 1, 2019, 13:00:00.000 (UTC+1)' - ); - }); -}); - -describe('TimestampTooltip', () => { - const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) - - beforeAll(() => { - // mock Date.now - mockNow(1570737000000); - - moment.tz.setDefault('America/Los_Angeles'); - }); - - afterAll(() => moment.tz.setDefault('')); - - it('should render component with relative time in body and absolute time in tooltip', () => { - expect(shallow()) - .toMatchInlineSnapshot(` - - 5 hours ago - - `); - }); - - it('should format with precision in milliseconds by default', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)'); - }); - - it('should format with precision in seconds', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06:40 (UTC-7)'); - }); - - it('should format with precision in minutes', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06 (UTC-7)'); - }); - - it('should format with precision in days', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019 (UTC-7)'); - }); -}); 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 d7ef6517c2fb..504ff36c078f 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 @@ -6,48 +6,20 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; import moment from 'moment-timezone'; +import { asAbsoluteDateTime, TimeUnit } from '../../../utils/formatters'; interface Props { /** * timestamp in milliseconds */ time: number; - precision?: 'days' | 'minutes' | 'seconds' | 'milliseconds'; + timeUnit?: TimeUnit; } -function getPreciseTime(precision: Props['precision']) { - switch (precision) { - case 'days': - return ''; - case 'minutes': - return ', HH:mm'; - case 'seconds': - return ', HH:mm:ss'; - default: - return ', HH:mm:ss.SSS'; - } -} - -function withLeadingPlus(value: number) { - return value > 0 ? `+${value}` : value; -} - -export function asAbsoluteTime({ time, precision = 'milliseconds' }: Props) { - const momentTime = moment(time); - const utcOffsetHours = momentTime.utcOffset() / 60; - const utcOffsetFormatted = Number.isInteger(utcOffsetHours) - ? withLeadingPlus(utcOffsetHours) - : 'Z'; - - return momentTime.format( - `MMM D, YYYY${getPreciseTime(precision)} (UTC${utcOffsetFormatted})` - ); -} - -export function TimestampTooltip({ time, precision = 'milliseconds' }: Props) { +export function TimestampTooltip({ time, timeUnit = 'milliseconds' }: Props) { const momentTime = moment(time); const relativeTimeLabel = momentTime.fromNow(); - const absoluteTimeLabel = asAbsoluteTime({ time, precision }); + const absoluteTimeLabel = asAbsoluteDateTime(time, timeUnit); return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 1bcf4e08c914..c4e7ed86df8b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -9,6 +9,7 @@ import numeral from '@elastic/numeral'; import { throttle } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { Maybe } from '../../../../../typings/common'; import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; import { asPercent } from '../../../../utils/formatters'; import { unit } from '../../../../style/variables'; @@ -19,7 +20,7 @@ interface Props { timeseries: TimeSeries[]; } -const tickFormatY = (y: number | null | undefined) => { +const tickFormatY = (y: Maybe) => { return numeral(y || 0).format('0 %'); }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js index b511bdc43922..f76a27480137 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js @@ -11,9 +11,8 @@ import d3 from 'd3'; import { HistogramInner } from '../index'; import response from './response.json'; import { - getTimeFormatter, asDecimal, - timeUnit + getDurationFormatter } from '../../../../../utils/formatters'; import { toJson } from '../../../../../utils/testHelpers'; import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index'; @@ -25,8 +24,7 @@ describe('Histogram', () => { beforeEach(() => { const buckets = getFormattedBuckets(response.buckets, response.bucketSize); const xMax = d3.max(buckets, d => d.x); - const timeFormatter = getTimeFormatter(xMax); - const unit = timeUnit(xMax); + const timeFormatter = getDurationFormatter(xMax); wrapper = mount( { bucketSize={response.bucketSize} transactionId="myTransactionId" onClick={onClick} - formatX={timeFormatter} + formatX={time => timeFormatter(time).formatted} formatYShort={t => `${asDecimal(t)} occ.`} formatYLong={t => `${asDecimal(t)} occurrences`} - tooltipHeader={bucket => - `${timeFormatter(bucket.x0, { - withUnit: false - })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` - } + tooltipHeader={bucket => { + const xFormatted = timeFormatter(bucket.x); + const x0Formatted = timeFormatter(bucket.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + }} width={800} /> ); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 51aa4a40fb92..30dcc99af31b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -9,20 +9,21 @@ import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform // @ts-ignore import CustomPlot from '../CustomPlot'; import { - asDynamicBytes, - asPercent, - getFixedByteFormatter, asDecimal, - asTime, - asInteger + asPercent, + asInteger, + asDynamicBytes, + getFixedByteFormatter, + asDuration } from '../../../../utils/formatters'; import { Coordinate } from '../../../../../typings/timeseries'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { useChartsSync } from '../../../../hooks/useChartsSync'; +import { Maybe } from '../../../../../typings/common'; interface Props { - start: number | string | undefined; - end: number | string | undefined; + start: Maybe; + end: Maybe; chart: GenericMetricsChart; } @@ -64,17 +65,17 @@ function getYTickFormatter(chart: GenericMetricsChart) { return getFixedByteFormatter(max); } case 'percent': { - return (y: number | null | undefined) => asPercent(y || 0, 1); + return (y: Maybe) => asPercent(y || 0, 1); } case 'time': { - return (y: number | null | undefined) => asTime(y); + return (y: Maybe) => asDuration(y); } case 'integer': { - return (y: number | null | undefined) => + return (y: Maybe) => isValidCoordinateValue(y) ? asInteger(y) : y; } default: { - return (y: number | null | undefined) => + return (y: Maybe) => isValidCoordinateValue(y) ? asDecimal(y) : y; } } @@ -89,7 +90,7 @@ function getTooltipFormatter({ yUnit }: GenericMetricsChart) { return (c: Coordinate) => asPercent(c.y || 0, 1); } case 'time': { - return (c: Coordinate) => asTime(c.y); + return (c: Coordinate) => asDuration(c.y); } case 'integer': { return (c: Coordinate) => diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js index 1f8c6db8d20a..8ee23d61fe0e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js @@ -10,7 +10,7 @@ import { EuiToolTip } from '@elastic/eui'; import Legend from '../Legend'; import { units, px } from '../../../../style/variables'; import styled from 'styled-components'; -import { asTime } from '../../../../utils/formatters'; +import { asDuration } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; const NameContainer = styled.div` @@ -39,7 +39,7 @@ export default function AgentMarker({ agentMark, x }) { content={
{agentMark.name} - {asTime(agentMark.us)} + {asDuration(agentMark.us)}
} > diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js index 1648f427edd7..346aec9fb080 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js @@ -12,7 +12,7 @@ import { XYPlot, XAxis } from 'react-vis'; import LastTickValue from './LastTickValue'; import AgentMarker from './AgentMarker'; import { px } from '../../../../style/variables'; -import { getTimeFormatter } from '../../../../utils/formatters'; +import { getDurationFormatter } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; // Remove any tick that is too close to topTraceDuration @@ -33,8 +33,9 @@ const getXAxisTickValues = (tickValues, topTraceDuration) => { function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues; - const tickFormat = getTimeFormatter(xMax); + const tickFormatter = getDurationFormatter(xMax); const xAxisTickValues = getXAxisTickValues(tickValues, topTraceDuration); + const topTraceDurationFormatted = tickFormatter(topTraceDuration).formatted; return ( @@ -66,7 +67,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { orientation="top" tickSize={0} tickValues={xAxisTickValues} - tickFormat={tickFormat} + tickFormat={time => tickFormatter(time).formatted} tickPadding={20} style={{ text: { fill: theme.euiColorDarkShade } @@ -76,7 +77,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { {topTraceDuration > 0 && ( )} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js index f5992ac7fc63..239e46c25904 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js @@ -19,7 +19,7 @@ import { } from '../../../../style/variables'; import Legend from '../Legend'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { asAbsoluteTime } from '../../TimestampTooltip'; +import { asAbsoluteDateTime } from '../../../../utils/formatters'; const TooltipElm = styled.div` margin: 0 ${px(unit)}; @@ -87,9 +87,7 @@ export default function Tooltip({ return ( -
- {header || asAbsoluteTime({ time: x, precision: 'seconds' })} -
+
{header || asAbsoluteDateTime(x, 'seconds')}
{showLegends ? ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx index adcce161c7ac..d2b6970841bd 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { asTime, asInteger } from '../../../../../utils/formatters'; +import { asDuration, asInteger } from '../../../../../utils/formatters'; import { fontSizes } from '../../../../../style/variables'; export const ChoroplethToolTip: React.SFC<{ @@ -26,7 +26,7 @@ export const ChoroplethToolTip: React.SFC<{ )}
- {asTime(value)} + {asDuration(value)}
( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 94f30a8a2325..b5894a9d91e4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -27,13 +27,13 @@ import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { asInteger, tpmUnit, - TimeFormatter + TimeFormatter, + getDurationFormatter } from '../../../../utils/formatters'; import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; import { LicenseContext } from '../../../../context/LicenseContext'; import { TransactionLineChart } from './TransactionLineChart'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { getTimeFormatter } from '../../../../utils/formatters'; import { DurationByCountryMap } from './DurationByCountryMap'; import { TRANSACTION_PAGE_LOAD, @@ -74,12 +74,14 @@ export class TransactionCharts extends Component { }; public getResponseTimeTickFormatter = (formatter: TimeFormatter) => { - return (t: number) => formatter(t); + return (t: number) => formatter(t).formatted; }; public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => { return (p: Coordinate) => { - return isValidCoordinateValue(p.y) ? formatter(p.y) : NOT_AVAILABLE_LABEL; + return isValidCoordinateValue(p.y) + ? formatter(p.y).formatted + : NOT_AVAILABLE_LABEL; }; }; @@ -154,7 +156,7 @@ export class TransactionCharts extends Component { const { responseTimeSeries, tpmSeries } = charts; const { transactionType } = urlParams; const maxY = this.getMaxY(responseTimeSeries); - const formatter = getTimeFormatter(maxY); + const formatter = getDurationFormatter(maxY); return ( <> diff --git a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts index b15231e89365..75a558ac81a5 100644 --- a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts @@ -16,7 +16,7 @@ import { RectCoordinate, TimeSeries } from '../../typings/timeseries'; -import { asDecimal, asMillis, tpmUnit } from '../utils/formatters'; +import { asDecimal, tpmUnit, convertTo } from '../utils/formatters'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries'; import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; @@ -70,6 +70,10 @@ export function getResponseTimeSeries({ }: TimeSeriesAPIResponse) { const { overallAvgDuration } = apmTimeseries; const { avg, p95, p99 } = apmTimeseries.responseTimes; + const formattedDuration = convertTo({ + unit: 'milliseconds', + microseconds: overallAvgDuration + }).formatted; const series: TimeSeries[] = [ { @@ -77,7 +81,7 @@ export function getResponseTimeSeries({ defaultMessage: 'Avg.' }), data: avg, - legendValue: asMillis(overallAvgDuration), + legendValue: formattedDuration, type: 'linemark', color: theme.euiColorVis1 }, diff --git a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts b/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts index 01a58ac03d0c..295ea1f9f900 100644 --- a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts +++ b/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts @@ -5,6 +5,7 @@ */ import { compact, isObject } from 'lodash'; +import { Maybe } from '../../typings/common'; export interface KeyValuePair { key: string; @@ -12,7 +13,7 @@ export interface KeyValuePair { } export const flattenObject = ( - item: Record | null | undefined, + item: Maybe>, parentKey?: string ): KeyValuePair[] => { if (item) { diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters.ts deleted file mode 100644 index 34b552230fa7..000000000000 --- a/x-pack/legacy/plugins/apm/public/utils/formatters.ts +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 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 numeral from '@elastic/numeral'; -import { i18n } from '@kbn/i18n'; -import { memoize } from 'lodash'; -import { NOT_AVAILABLE_LABEL } from '../../common/i18n'; - -const HOURS_CUT_OFF = 3600000000; // 1 hour (in microseconds) -const MINUTES_CUT_OFF = 60000000; // 1 minute (in microseconds) -const SECONDS_CUT_OFF = 10 * 1000000; // 10 seconds (in microseconds) -const MILLISECONDS_CUT_OFF = 10 * 1000; // 10 milliseconds (in microseconds) -const SPACE = ' '; - -/* - * value: time in microseconds - * withUnit: add unit suffix - * defaultValue: value to use if the specified is null/undefined - */ -type FormatterValue = number | undefined | null; -interface FormatterOptions { - withUnit?: boolean; - defaultValue?: string; -} - -export function asHours( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const hoursLabel = - SPACE + - i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { - defaultMessage: 'h' - }); - const formatted = asDecimal(value / 3600000000); - return `${formatted}${withUnit ? hoursLabel : ''}`; -} - -export function asMinutes( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const minutesLabel = - SPACE + - i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { - defaultMessage: 'min' - }); - const formatted = asDecimal(value / 60000000); - return `${formatted}${withUnit ? minutesLabel : ''}`; -} - -export function asSeconds( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const secondsLabel = - SPACE + - i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { - defaultMessage: 's' - }); - const formatted = asDecimal(value / 1000000); - return `${formatted}${withUnit ? secondsLabel : ''}`; -} - -export function asMillis( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - - const millisLabel = - SPACE + - i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { - defaultMessage: 'ms' - }); - const formatted = asInteger(value / 1000); - return `${formatted}${withUnit ? millisLabel : ''}`; -} - -export function asMicros( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - - const microsLabel = - SPACE + - i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { - defaultMessage: 'μs' - }); - const formatted = asInteger(value); - return `${formatted}${withUnit ? microsLabel : ''}`; -} - -export type TimeFormatter = ( - value: FormatterValue, - options?: FormatterOptions -) => string; - -type TimeFormatterBuilder = (max: number) => TimeFormatter; - -export const getTimeFormatter: TimeFormatterBuilder = memoize((max: number) => { - const unit = timeUnit(max); - switch (unit) { - case 'h': - return asHours; - case 'm': - return asMinutes; - case 's': - return asSeconds; - case 'ms': - return asMillis; - case 'us': - return asMicros; - } -}); - -export function timeUnit(max: number) { - if (max > HOURS_CUT_OFF) { - return 'h'; - } else if (max > MINUTES_CUT_OFF) { - return 'm'; - } else if (max > SECONDS_CUT_OFF) { - return 's'; - } else if (max > MILLISECONDS_CUT_OFF) { - return 'ms'; - } else { - return 'us'; - } -} - -export function asTime( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const formatter = getTimeFormatter(value); - return formatter(value, { withUnit, defaultValue }); -} - -export function asDecimal(value: number) { - return numeral(value).format('0,0.0'); -} - -export function asInteger(value: number) { - return numeral(value).format('0,0'); -} - -export function tpmUnit(type?: string) { - return type === 'request' - ? i18n.translate('xpack.apm.formatters.requestsPerMinLabel', { - defaultMessage: 'rpm' - }) - : i18n.translate('xpack.apm.formatters.transactionsPerMinLabel', { - defaultMessage: 'tpm' - }); -} - -export function asPercent( - numerator: number, - denominator: number | undefined, - fallbackResult = '' -) { - if (!denominator || isNaN(numerator)) { - return fallbackResult; - } - - const decimal = numerator / denominator; - return numeral(decimal).format('0.0%'); -} - -function asKilobytes(value: number) { - return `${asDecimal(value / 1000)} KB`; -} - -function asMegabytes(value: number) { - return `${asDecimal(value / 1e6)} MB`; -} - -function asGigabytes(value: number) { - return `${asDecimal(value / 1e9)} GB`; -} - -function asTerabytes(value: number) { - return `${asDecimal(value / 1e12)} TB`; -} - -function asBytes(value: number) { - return `${asDecimal(value)} B`; -} - -const bailIfNumberInvalid = (cb: (val: number) => string) => { - return (val: number | null | undefined) => { - if (val === null || val === undefined || isNaN(val)) { - return ''; - } - return cb(val); - }; -}; - -export const asDynamicBytes = bailIfNumberInvalid((value: number) => { - return unmemoizedFixedByteFormatter(value)(value); -}); - -const unmemoizedFixedByteFormatter = (max: number) => { - if (max > 1e12) { - return asTerabytes; - } - - if (max > 1e9) { - return asGigabytes; - } - - if (max > 1e6) { - return asMegabytes; - } - - if (max > 1000) { - return asKilobytes; - } - - return asBytes; -}; - -export const getFixedByteFormatter = memoize((max: number) => { - const formatter = unmemoizedFixedByteFormatter(max); - - return bailIfNumberInvalid(formatter); -}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts new file mode 100644 index 000000000000..bec9cede00a2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts @@ -0,0 +1,146 @@ +/* + * 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'; +import { asRelativeDateTimeRange, asAbsoluteDateTime } from '../datetime'; + +describe('date time formatters', () => { + describe('asRelativeDateTimeRange', () => { + beforeAll(() => { + moment.tz.setDefault('Europe/Amsterdam'); + }); + afterAll(() => moment.tz.setDefault('')); + const formatDateToTimezone = (dateTimeString: string) => + moment(dateTimeString).valueOf(); + + describe('YYYY - YYYY', () => { + it('range: 10 years', () => { + const start = formatDateToTimezone('2000-01-01 10:01:01'); + const end = formatDateToTimezone('2010-01-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('2000 - 2010'); + }); + it('range: 5 years', () => { + const start = formatDateToTimezone('2010-01-01 10:01:01'); + const end = formatDateToTimezone('2015-01-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('2010 - 2015'); + }); + }); + describe('MMM YYYY - MMM YYYY', () => { + it('range: 4 years ', () => { + const start = formatDateToTimezone('2010-01-01 10:01:01'); + const end = formatDateToTimezone('2014-04-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Jan 2010 - Apr 2014'); + }); + it('range: 6 months ', () => { + const start = formatDateToTimezone('2019-01-01 10:01:01'); + const end = formatDateToTimezone('2019-07-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Jan 2019 - Jul 2019'); + }); + }); + describe('MMM D, YYYY - MMM D, YYYY', () => { + it('range: 2 days', () => { + const start = formatDateToTimezone('2019-10-01 10:01:01'); + const end = formatDateToTimezone('2019-10-05 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 1, 2019 - Oct 5, 2019'); + }); + it('range: 1 day', () => { + const start = formatDateToTimezone('2019-10-01 10:01:01'); + const end = formatDateToTimezone('2019-10-03 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 1, 2019 - Oct 3, 2019'); + }); + }); + describe('MMM D, YYYY, HH:mm - HH:mm (UTC)', () => { + it('range: 9 hours', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 19:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01 - 19:01 (UTC+1)'); + }); + it('range: 5 hours', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 15:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01 - 15:01 (UTC+1)'); + }); + }); + describe('MMM D, YYYY, HH:mm:ss - HH:mm:ss (UTC)', () => { + it('range: 14 minutes', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 10:15:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01:01 - 10:15:01 (UTC+1)'); + }); + it('range: 5 minutes', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 10:06:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01:01 - 10:06:01 (UTC+1)'); + }); + }); + describe('MMM D, YYYY, HH:mm:ss.SSS - HH:mm:ss.SSS (UTC)', () => { + it('range: 9 seconds', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01.001'); + const end = formatDateToTimezone('2019-10-29 10:01:10.002'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual( + 'Oct 29, 2019, 10:01:01.001 - 10:01:10.002 (UTC+1)' + ); + }); + it('range: 1 second', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01.001'); + const end = formatDateToTimezone('2019-10-29 10:01:02.002'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual( + 'Oct 29, 2019, 10:01:01.001 - 10:01:02.002 (UTC+1)' + ); + }); + }); + }); + + describe('asAbsoluteDateTime', () => { + afterAll(() => moment.tz.setDefault('')); + + it('should add a leading plus for timezones with positive UTC offset', () => { + moment.tz.setDefault('Europe/Copenhagen'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 14:00 (UTC+2)' + ); + }); + + it('should add a leading minus for timezones with negative UTC offset', () => { + moment.tz.setDefault('America/Los_Angeles'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 05:00 (UTC-7)' + ); + }); + + it('should use default UTC offset formatting when offset contains minutes', () => { + moment.tz.setDefault('Canada/Newfoundland'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 09:30 (UTC-02:30)' + ); + }); + + it('should respect DST', () => { + moment.tz.setDefault('Europe/Copenhagen'); + const timeWithDST = 1559390400000; // Jun 1, 2019 + const timeWithoutDST = 1575201600000; // Dec 1, 2019 + + expect(asAbsoluteDateTime(timeWithDST)).toBe( + 'Jun 1, 2019, 14:00:00.000 (UTC+2)' + ); + + expect(asAbsoluteDateTime(timeWithoutDST)).toBe( + 'Dec 1, 2019, 13:00:00.000 (UTC+1)' + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts new file mode 100644 index 000000000000..014ecad01d4d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { asDuration, convertTo, toMicroseconds } from '../duration'; + +describe('duration formatters', () => { + describe('asDuration', () => { + it('formats correctly with defaults', () => { + expect(asDuration(null)).toEqual('N/A'); + expect(asDuration(undefined)).toEqual('N/A'); + expect(asDuration(0)).toEqual('0 μs'); + expect(asDuration(1)).toEqual('1 μs'); + expect(asDuration(toMicroseconds(1, 'milliseconds'))).toEqual('1,000 μs'); + expect(asDuration(toMicroseconds(1000, 'milliseconds'))).toEqual( + '1,000 ms' + ); + expect(asDuration(toMicroseconds(10000, 'milliseconds'))).toEqual( + '10,000 ms' + ); + expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20.0 s'); + expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('10.0 min'); + expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60.0 min'); + expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('1.5 h'); + }); + + it('falls back to default value', () => { + expect(asDuration(undefined, { defaultValue: 'nope' })).toEqual('nope'); + }); + }); + + describe('convertTo', () => { + it('hours', () => { + const unit = 'hours'; + const oneHourAsMicro = toMicroseconds(1, 'hours'); + const twoHourAsMicro = toMicroseconds(2, 'hours'); + expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({ + unit: 'h', + value: '1.0', + formatted: '1.0 h' + }); + expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({ + unit: 'h', + value: '2.0', + formatted: '2.0 h' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '1.2' }) + ).toEqual({ value: '1.2', formatted: '1.2' }); + }); + + it('minutes', () => { + const unit = 'minutes'; + const oneHourAsMicro = toMicroseconds(1, 'hours'); + const twoHourAsMicro = toMicroseconds(2, 'hours'); + expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({ + unit: 'min', + value: '60.0', + formatted: '60.0 min' + }); + expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({ + unit: 'min', + value: '120.0', + formatted: '120.0 min' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('seconds', () => { + const unit = 'seconds'; + const twentySecondsAsMicro = toMicroseconds(20, 'seconds'); + const thirtyFiveSecondsAsMicro = toMicroseconds(35, 'seconds'); + expect(convertTo({ unit, microseconds: twentySecondsAsMicro })).toEqual({ + unit: 's', + value: '20.0', + formatted: '20.0 s' + }); + expect( + convertTo({ unit, microseconds: thirtyFiveSecondsAsMicro }) + ).toEqual({ unit: 's', value: '35.0', formatted: '35.0 s' }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('milliseconds', () => { + const unit = 'milliseconds'; + const twentyMilliAsMicro = toMicroseconds(20, 'milliseconds'); + const thirtyFiveMilliAsMicro = toMicroseconds(35, 'milliseconds'); + expect(convertTo({ unit, microseconds: twentyMilliAsMicro })).toEqual({ + unit: 'ms', + value: '20', + formatted: '20 ms' + }); + expect( + convertTo({ unit, microseconds: thirtyFiveMilliAsMicro }) + ).toEqual({ unit: 'ms', value: '35', formatted: '35 ms' }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('microseconds', () => { + const unit = 'microseconds'; + expect(convertTo({ unit, microseconds: 20 })).toEqual({ + unit: 'μs', + value: '20', + formatted: '20 μs' + }); + expect(convertTo({ unit, microseconds: 35 })).toEqual({ + unit: 'μs', + value: '35', + formatted: '35 μs' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + }); + describe('toMicroseconds', () => { + it('transformes to microseconds', () => { + expect(toMicroseconds(1, 'hours')).toEqual(3600000000); + expect(toMicroseconds(10, 'minutes')).toEqual(600000000); + expect(toMicroseconds(10, 'seconds')).toEqual(10000000); + expect(toMicroseconds(10, 'milliseconds')).toEqual(10000); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts new file mode 100644 index 000000000000..f6ed88a850a5 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { asPercent } from '../formatters'; + +describe('formatters', () => { + describe('asPercent', () => { + it('should divide and format item as percent', () => { + expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%'); + }); + + it('should format when numerator is 0', () => { + expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); + }); + + it('should return fallback when denominator is undefined', () => { + expect(asPercent(3725, undefined, 'n/a')).toEqual('n/a'); + }); + + it('should return fallback when denominator is 0 ', () => { + expect(asPercent(3725, 0, 'n/a')).toEqual('n/a'); + }); + + it('should return fallback when numerator or denominator is NaN', () => { + expect(asPercent(3725, NaN, 'n/a')).toEqual('n/a'); + expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts similarity index 63% rename from x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts rename to x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts index 093624240565..07d3d0c1eb08 100644 --- a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts @@ -3,61 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { getFixedByteFormatter, asDynamicBytes } from '../size'; -import { - asPercent, - asTime, - getFixedByteFormatter, - asDynamicBytes -} from '../formatters'; - -describe('formatters', () => { - describe('asTime', () => { - it('formats correctly with defaults', () => { - expect(asTime(null)).toEqual('N/A'); - expect(asTime(undefined)).toEqual('N/A'); - expect(asTime(0)).toEqual('0 μs'); - expect(asTime(1)).toEqual('1 μs'); - expect(asTime(1000)).toEqual('1,000 μs'); - expect(asTime(1000 * 1000)).toEqual('1,000 ms'); - expect(asTime(1000 * 1000 * 10)).toEqual('10,000 ms'); - expect(asTime(1000 * 1000 * 20)).toEqual('20.0 s'); - expect(asTime(60000000 * 10)).toEqual('10.0 min'); - expect(asTime(3600000000 * 1.5)).toEqual('1.5 h'); - }); - - it('formats without unit', () => { - expect(asTime(1000, { withUnit: false })).toEqual('1,000'); - }); - - it('falls back to default value', () => { - expect(asTime(undefined, { defaultValue: 'nope' })).toEqual('nope'); - }); - }); - - describe('asPercent', () => { - it('should divide and format item as percent', () => { - expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%'); - }); - - it('should format when numerator is 0', () => { - expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); - }); - - it('should return fallback when denominator is undefined', () => { - expect(asPercent(3725, undefined, 'n/a')).toEqual('n/a'); - }); - - it('should return fallback when denominator is 0 ', () => { - expect(asPercent(3725, 0, 'n/a')).toEqual('n/a'); - }); - - it('should return fallback when numerator or denominator is NaN', () => { - expect(asPercent(3725, NaN, 'n/a')).toEqual('n/a'); - expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a'); - }); - }); - +describe('size formatters', () => { describe('byte formatting', () => { const bytes = 10; const kb = 1000 + 1; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts new file mode 100644 index 000000000000..98483a0351f0 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts @@ -0,0 +1,149 @@ +/* + * 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'; + +/** + * Returns the timezone set on momentTime. + * (UTC+offset) when offset if bigger than 0. + * (UTC-offset) when offset if lower than 0. + * @param momentTime Moment + */ +function formatTimezone(momentTime: moment.Moment) { + const DEFAULT_TIMEZONE_FORMAT = 'Z'; + + const utcOffsetHours = momentTime.utcOffset() / 60; + + const customTimezoneFormat = + utcOffsetHours > 0 ? `+${utcOffsetHours}` : utcOffsetHours; + + const utcOffsetFormatted = Number.isInteger(utcOffsetHours) + ? customTimezoneFormat + : DEFAULT_TIMEZONE_FORMAT; + + return momentTime.format(`(UTC${utcOffsetFormatted})`); +} + +export type TimeUnit = 'hours' | 'minutes' | 'seconds' | 'milliseconds'; +function getTimeFormat(timeUnit: TimeUnit) { + switch (timeUnit) { + case 'hours': + return 'HH'; + case 'minutes': + return 'HH:mm'; + case 'seconds': + return 'HH:mm:ss'; + case 'milliseconds': + return 'HH:mm:ss.SSS'; + default: + return ''; + } +} + +type DateUnit = 'days' | 'months' | 'years'; +function getDateFormat(dateUnit: DateUnit) { + switch (dateUnit) { + case 'years': + return 'YYYY'; + case 'months': + return 'MMM YYYY'; + case 'days': + return 'MMM D, YYYY'; + default: + return ''; + } +} + +function getFormatsAccordingToDateDifference( + momentStart: moment.Moment, + momentEnd: moment.Moment +) { + const getDateDifference = (unitOfTime: DateUnit | TimeUnit) => + momentEnd.diff(momentStart, unitOfTime); + + if (getDateDifference('years') >= 5) { + return { dateFormat: getDateFormat('years') }; + } + + if (getDateDifference('months') >= 5) { + return { dateFormat: getDateFormat('months') }; + } + + const dateFormatWithDays = getDateFormat('days'); + if (getDateDifference('days') > 1) { + return { dateFormat: dateFormatWithDays }; + } + + if (getDateDifference('hours') >= 5) { + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('minutes') + }; + } + + if (getDateDifference('minutes') >= 5) { + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('seconds') + }; + } + + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('milliseconds') + }; +} + +export function asAbsoluteDateTime( + time: number, + timeUnit: TimeUnit = 'milliseconds' +) { + const momentTime = moment(time); + const formattedTz = formatTimezone(momentTime); + + return momentTime.format( + `${getDateFormat('days')}, ${getTimeFormat(timeUnit)} ${formattedTz}` + ); +} + +/** + * + * Returns the dates formatted according to the difference between the two dates: + * + * | Difference | Format | + * | -------------- |:----------------------------------------------:| + * | >= 5 years | YYYY - YYYY | + * | >= 5 months | MMM YYYY - MMM YYYY | + * | > 1 day | MMM D, YYYY - MMM D, YYYY | + * | >= 5 hours | MMM D, YYYY, HH:mm - HH:mm (UTC) | + * | >= 5 minutes | MMM D, YYYY, HH:mm:ss - HH:mm:ss (UTC) | + * | default | MMM D, YYYY, HH:mm:ss.SSS - HH:mm:ss.SSS (UTC) | + * + * @param start timestamp + * @param end timestamp + */ +export function asRelativeDateTimeRange(start: number, end: number) { + const momentStartTime = moment(start); + const momentEndTime = moment(end); + + const { dateFormat, timeFormat } = getFormatsAccordingToDateDifference( + momentStartTime, + momentEndTime + ); + + if (timeFormat) { + const startFormatted = momentStartTime.format( + `${dateFormat}, ${timeFormat}` + ); + const endFormatted = momentEndTime.format(timeFormat); + const formattedTz = formatTimezone(momentStartTime); + return `${startFormatted} - ${endFormatted} ${formattedTz}`; + } + + const startFormatted = momentStartTime.format(dateFormat); + const endFormatted = momentEndTime.format(dateFormat); + return `${startFormatted} - ${endFormatted}`; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts new file mode 100644 index 000000000000..39341e1ff444 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts @@ -0,0 +1,153 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { memoize } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; +import { asDecimal, asInteger } from './formatters'; +import { TimeUnit } from './datetime'; +import { Maybe } from '../../../typings/common'; + +interface FormatterOptions { + defaultValue?: string; +} + +type DurationTimeUnit = TimeUnit | 'microseconds'; + +interface DurationUnit { + [unit: string]: { + label: string; + convert: (value: number) => string; + }; +} + +interface ConvertedDuration { + value: string; + unit?: string; + formatted: string; +} + +export type TimeFormatter = ( + value: Maybe, + options?: FormatterOptions +) => ConvertedDuration; + +type TimeFormatterBuilder = (max: number) => TimeFormatter; + +const durationUnit: DurationUnit = { + hours: { + label: i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { + defaultMessage: 'h' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asHours()) + }, + minutes: { + label: i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { + defaultMessage: 'min' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asMinutes()) + }, + seconds: { + label: i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { + defaultMessage: 's' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asSeconds()) + }, + milliseconds: { + label: i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { + defaultMessage: 'ms' + }), + convert: (value: number) => + asInteger(moment.duration(value / 1000).asMilliseconds()) + }, + microseconds: { + label: i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { + defaultMessage: 'μs' + }), + convert: (value: number) => asInteger(value) + } +}; + +/** + * Converts a microseconds value into the unit defined. + * + * @param param0 + * { unit: "milliseconds" | "hours" | "minutes" | "seconds" | "microseconds", microseconds, defaultValue } + * + * @returns object { value, unit, formatted } + */ +export function convertTo({ + unit, + microseconds, + defaultValue = NOT_AVAILABLE_LABEL +}: { + unit: DurationTimeUnit; + microseconds: Maybe; + defaultValue?: string; +}): ConvertedDuration { + const duration = durationUnit[unit]; + if (!duration || microseconds == null) { + return { value: defaultValue, formatted: defaultValue }; + } + + const convertedValue = duration.convert(microseconds); + return { + value: convertedValue, + unit: duration.label, + formatted: `${convertedValue} ${duration.label}` + }; +} + +export const toMicroseconds = (value: number, timeUnit: TimeUnit) => + moment.duration(value, timeUnit).asMilliseconds() * 1000; + +function getDurationUnitKey(max: number): DurationTimeUnit { + if (max > toMicroseconds(1, 'hours')) { + return 'hours'; + } + if (max > toMicroseconds(1, 'minutes')) { + return 'minutes'; + } + if (max > toMicroseconds(10, 'seconds')) { + return 'seconds'; + } + if (max > toMicroseconds(10, 'milliseconds')) { + return 'milliseconds'; + } + return 'microseconds'; +} + +export const getDurationFormatter: TimeFormatterBuilder = memoize( + (max: number) => { + const unit = getDurationUnitKey(max); + return (value, { defaultValue }: FormatterOptions = {}) => { + return convertTo({ unit, microseconds: value, defaultValue }); + }; + } +); + +/** + * Converts value and returns it formatted - 00 unit + * + * @param value + * @param param1 { defaultValue } + * @returns formated value - 00 unit + */ +export function asDuration( + value: Maybe, + { defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} +) { + if (value == null) { + return defaultValue; + } + + const formatter = getDurationFormatter(value); + return formatter(value, { defaultValue }).formatted; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts new file mode 100644 index 000000000000..630b6a0a18db --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts @@ -0,0 +1,38 @@ +/* + * 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 numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; + +export function asDecimal(value: number) { + return numeral(value).format('0,0.0'); +} + +export function asInteger(value: number) { + return numeral(value).format('0,0'); +} + +export function tpmUnit(type?: string) { + return type === 'request' + ? i18n.translate('xpack.apm.formatters.requestsPerMinLabel', { + defaultMessage: 'rpm' + }) + : i18n.translate('xpack.apm.formatters.transactionsPerMinLabel', { + defaultMessage: 'tpm' + }); +} + +export function asPercent( + numerator: number, + denominator: number | undefined, + fallbackResult = '' +) { + if (!denominator || isNaN(numerator)) { + return fallbackResult; + } + + const decimal = numerator / denominator; + return numeral(decimal).format('0.0%'); +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts new file mode 100644 index 000000000000..4fedd55ff1e8 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export * from './formatters'; +export * from './datetime'; +export * from './duration'; +export * from './size'; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts new file mode 100644 index 000000000000..2cdf8af1d46d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts @@ -0,0 +1,67 @@ +/* + * 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 { memoize } from 'lodash'; +import { asDecimal } from './formatters'; +import { Maybe } from '../../../typings/common'; + +function asKilobytes(value: number) { + return `${asDecimal(value / 1000)} KB`; +} + +function asMegabytes(value: number) { + return `${asDecimal(value / 1e6)} MB`; +} + +function asGigabytes(value: number) { + return `${asDecimal(value / 1e9)} GB`; +} + +function asTerabytes(value: number) { + return `${asDecimal(value / 1e12)} TB`; +} + +function asBytes(value: number) { + return `${asDecimal(value)} B`; +} + +const bailIfNumberInvalid = (cb: (val: number) => string) => { + return (val: Maybe) => { + if (val === null || val === undefined || isNaN(val)) { + return ''; + } + return cb(val); + }; +}; + +export const getFixedByteFormatter = memoize((max: number) => { + const formatter = unmemoizedFixedByteFormatter(max); + + return bailIfNumberInvalid(formatter); +}); + +export const asDynamicBytes = bailIfNumberInvalid((value: number) => { + return unmemoizedFixedByteFormatter(value)(value); +}); + +const unmemoizedFixedByteFormatter = (max: number) => { + if (max > 1e12) { + return asTerabytes; + } + + if (max > 1e9) { + return asGigabytes; + } + + if (max > 1e6) { + return asMegabytes; + } + + if (max > 1000) { + return asKilobytes; + } + + return asBytes; +}; diff --git a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts b/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts index 411d03fce349..c36efc232b78 100644 --- a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts +++ b/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Maybe } from '../../typings/common'; -export const isValidCoordinateValue = ( - value: number | null | undefined -): value is number => value !== null && value !== undefined; +export const isValidCoordinateValue = (value: Maybe): value is number => + value !== null && value !== undefined; diff --git a/x-pack/legacy/plugins/apm/typings/common.d.ts b/x-pack/legacy/plugins/apm/typings/common.d.ts index d79b05ed99b4..b9064980bd65 100644 --- a/x-pack/legacy/plugins/apm/typings/common.d.ts +++ b/x-pack/legacy/plugins/apm/typings/common.d.ts @@ -27,3 +27,5 @@ export type PromiseReturnType = Func extends ( ) => Promise ? Value : Func; + +export type Maybe = T | null | undefined; diff --git a/x-pack/legacy/plugins/apm/typings/timeseries.ts b/x-pack/legacy/plugins/apm/typings/timeseries.ts index 9b9f7dcc2c82..d64486d8e71e 100644 --- a/x-pack/legacy/plugins/apm/typings/timeseries.ts +++ b/x-pack/legacy/plugins/apm/typings/timeseries.ts @@ -3,10 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Maybe } from '../typings/common'; export interface Coordinate { x: number; - y: number | null | undefined; + y: Maybe; } export interface RectCoordinate {