{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 {