diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js index 419729e4898fc..76317a51fa6f2 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js @@ -10,16 +10,17 @@ import styled from 'styled-components'; import { RelativeLink } from '../../../../utils/url'; import { fontSizes, truncate } from '../../../../style/variables'; import TooltipOverlay from '../../../shared/TooltipOverlay'; -import { asMillisWithDefault, asDecimal } from '../../../../utils/formatters'; +import { asMillis, asDecimal } from '../../../../utils/formatters'; import { ManagedTable } from '../../../shared/ManagedTable'; -// TODO: Consolidate these formatting helpers centrally function formatNumber(value) { if (value === 0) { return '0'; + } else if (value <= 0.1) { + return '< 0.1'; + } else { + return asDecimal(value); } - const formatted = asDecimal(value); - return formatted <= 0.1 ? '< 0.1' : formatted; } function formatString(value) { @@ -56,7 +57,7 @@ const SERVICE_COLUMNS = [ name: 'Avg. response time', sortable: true, dataType: 'number', - render: value => asMillisWithDefault(value) + render: value => asMillis(value) }, { field: 'transactionsPerMinute', diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index b2c2dbd28874d..0de0fb0c183aa 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -8,7 +8,7 @@ import React from 'react'; import styled from 'styled-components'; import { ITransactionGroup } from '../../../../typings/TransactionGroup'; import { fontSizes, truncate } from '../../../style/variables'; -import { asMillisWithDefault } from '../../../utils/formatters'; +import { asMillis } from '../../../utils/formatters'; import { ImpactBar } from '../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; // @ts-ignore @@ -50,7 +50,7 @@ const traceListColumns: ITableColumn[] = [ name: 'Avg. response time', sortable: true, dataType: 'number', - render: (value: number) => asMillisWithDefault(value) + render: (value: number) => asMillis(value) }, { field: 'transactionsPerMinute', diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 0cc1185b79c92..f7321df130660 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -133,9 +133,9 @@ export class Distribution extends Component { bucket.y > 0 && bucket.sample } tooltipHeader={(bucket: IChartPoint) => - `${timeFormatter(bucket.x0, false)} - ${timeFormatter( + `${timeFormatter(bucket.x0, { withUnit: false })} - ${timeFormatter( bucket.x, - false + { withUnit: false } )} ${unit}` } tooltipFooter={(bucket: IChartPoint) => diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx index 1e8fe00a8f382..a8330c1213138 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx @@ -50,7 +50,7 @@ export function StickyTransactionProperties({ { label: 'Duration', fieldName: TRANSACTION_DURATION, - val: duration ? asTime(duration) : 'N/A', + val: asTime(duration), width: '25%' }, { diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js index 52d8d6f4b3433..54657ded388f5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js @@ -8,11 +8,7 @@ import React from 'react'; import styled from 'styled-components'; import TooltipOverlay from '../../../shared/TooltipOverlay'; import { RelativeLink, legacyEncodeURIComponent } from '../../../../utils/url'; -import { - asMillisWithDefault, - asDecimal, - tpmUnit -} from '../../../../utils/formatters'; +import { asMillis, asDecimal, tpmUnit } from '../../../../utils/formatters'; import { ImpactBar } from '../../../shared/ImpactBar'; import { fontFamilyCode, truncate } from '../../../../style/variables'; @@ -63,14 +59,14 @@ export default function TransactionList({ name: avgLabel(agentName), sortable: true, dataType: 'number', - render: value => asMillisWithDefault(value) + render: value => asMillis(value) }, { field: 'p95', name: '95th percentile', sortable: true, dataType: 'number', - render: value => asMillisWithDefault(value) + render: value => asMillis(value) }, { field: 'transactionsPerMinute', diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js index 9b1c462ed201e..615de4c170065 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js @@ -38,9 +38,9 @@ describe('Histogram', () => { formatYShort={t => `${asDecimal(t)} occ.`} formatYLong={t => `${asDecimal(t)} occurrences`} tooltipHeader={bucket => - `${timeFormatter(bucket.x0, false)} - ${timeFormatter( + `${timeFormatter(bucket.x0, { withUnit: false })} - ${timeFormatter( bucket.x, - false + { withUnit: false } )} ${unit}` } width={800} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap index 2dfc2dfe57967..24e63fa08719d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap @@ -115,7 +115,7 @@ exports[`Histogram Initially should have default markup 1`] = ` textAnchor="middle" transform="translate(0, 18)" > - 0 + 0 ms - 0 + 0 ms { - if (this.props.charts.noHits) { - return '- ms'; - } else { - return p.y == null ? 'N/A' : asMillis(p.y); - } + return this.props.charts.noHits ? '- ms' : asMillis(p.y); }; getTPMFormatter = t => { diff --git a/x-pack/plugins/apm/public/store/selectors/chartSelectors.js b/x-pack/plugins/apm/public/store/selectors/chartSelectors.js index cc898d6b66779..534da52262d16 100644 --- a/x-pack/plugins/apm/public/store/selectors/chartSelectors.js +++ b/x-pack/plugins/apm/public/store/selectors/chartSelectors.js @@ -7,11 +7,7 @@ import d3 from 'd3'; import { last, zipObject, difference, memoize, get, isEmpty } from 'lodash'; import { colors } from '../../style/variables'; -import { - asMillisWithDefault, - asDecimal, - tpmUnit -} from '../../utils/formatters'; +import { asMillis, asDecimal, tpmUnit } from '../../utils/formatters'; import { rgba } from 'polished'; export const getEmptySerie = memoize( @@ -59,7 +55,7 @@ export function getResponseTimeSeries(chartsData) { { title: 'Avg.', data: getChartValues(dates, avg), - legendValue: `${asMillisWithDefault(overallAvgDuration)}`, + legendValue: asMillis(overallAvgDuration), type: 'line', color: colors.apmBlue }, diff --git a/x-pack/plugins/apm/public/utils/__test__/formatters.test.ts b/x-pack/plugins/apm/public/utils/__test__/formatters.test.ts index f0a119ccf0202..46909496e80a8 100644 --- a/x-pack/plugins/apm/public/utils/__test__/formatters.test.ts +++ b/x-pack/plugins/apm/public/utils/__test__/formatters.test.ts @@ -7,11 +7,25 @@ import { asPercent, asTime } from '../formatters'; describe('formatters', () => { - it('asTime', () => { - expect(asTime(1000)).toBe('1 ms'); - expect(asTime(1000 * 1000)).toBe('1,000 ms'); - expect(asTime(1000 * 1000 * 10)).toBe('10,000 ms'); - expect(asTime(1000 * 1000 * 20)).toBe('20.0 s'); + describe('asTime', () => { + it('formats correctly with defaults', () => { + expect(asTime(null)).toBe('N/A'); + expect(asTime(undefined)).toBe('N/A'); + expect(asTime(0)).toBe('0 μs'); + expect(asTime(1)).toBe('1 μs'); + expect(asTime(1000)).toBe('1,000 μs'); + expect(asTime(1000 * 1000)).toBe('1,000 ms'); + expect(asTime(1000 * 1000 * 10)).toBe('10,000 ms'); + expect(asTime(1000 * 1000 * 20)).toBe('20.0 s'); + }); + + it('formats without unit', () => { + expect(asTime(1000, { withUnit: false })).toBe('1,000'); + }); + + it('falls back to default value', () => { + expect(asTime(undefined, { defaultValue: 'nope' })).toBe('nope'); + }); }); describe('asPercent', () => { diff --git a/x-pack/plugins/apm/public/utils/formatters.ts b/x-pack/plugins/apm/public/utils/formatters.ts index 0c6dfbc84a8cc..08551aa43b7ac 100644 --- a/x-pack/plugins/apm/public/utils/formatters.ts +++ b/x-pack/plugins/apm/public/utils/formatters.ts @@ -4,60 +4,107 @@ * you may not use this file except in compliance with the Elastic License. */ +import numeral from '@elastic/numeral'; import { memoize } from 'lodash'; -// tslint:disable-next-line no-var-requires -const numeral: (input: number) => Numeral = require('@elastic/numeral'); +const SECONDS_CUT_OFF = 10 * 1000000; // 10 seconds (in microseconds) +const MILLISECONDS_CUT_OFF = 10 * 1000; // 10 milliseconds (in microseconds) -interface Numeral { - format: (pattern: string) => string; +/* + * 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; } -const UNIT_CUT_OFF = 10 * 1000000; // 10 seconds in microseconds - -export function asSeconds(value: number, withUnit = true) { +export function asSeconds( + value: FormatterValue, + { withUnit = true, defaultValue = 'N/A' }: FormatterOptions = {} +) { + if (value == null) { + return defaultValue; + } const formatted = asDecimal(value / 1000000); return `${formatted}${withUnit ? ' s' : ''}`; } -export function asMillis(value: number, withUnit = true) { +export function asMillis( + value: FormatterValue, + { withUnit = true, defaultValue = 'N/A' }: FormatterOptions = {} +) { + if (value == null) { + return defaultValue; + } + const formatted = asInteger(value / 1000); return `${formatted}${withUnit ? ' ms' : ''}`; } -export function asMillisWithDefault(value?: number) { +export function asMicros( + value: FormatterValue, + { withUnit = true, defaultValue = 'N/A' }: FormatterOptions = {} +) { if (value == null) { - return `N/A`; + return defaultValue; } - return asMillis(value); + + const formatted = asInteger(value); + return `${formatted}${withUnit ? ' μs' : ''}`; } -export const getTimeFormatter: ( +type TimeFormatter = ( max: number -) => (value: number, withUnit?: boolean) => string = memoize( - (max: number) => (max > UNIT_CUT_OFF ? asSeconds : asMillis) -); +) => ( + value: FormatterValue, + { withUnit, defaultValue }: FormatterOptions +) => string; + +export const getTimeFormatter: TimeFormatter = memoize((max: number) => { + const unit = timeUnit(max); + switch (unit) { + case 's': + return asSeconds; + case 'ms': + return asMillis; + case 'us': + return asMicros; + } +}); export function timeUnit(max: number) { - return max > UNIT_CUT_OFF ? 's' : 'ms'; + if (max > SECONDS_CUT_OFF) { + return 's'; + } else if (max > MILLISECONDS_CUT_OFF) { + return 'ms'; + } else { + return 'us'; + } } -/* - * value: time in microseconds - */ -export function asTime(value: number): string { - return getTimeFormatter(value)(value); +export function asTime( + value: FormatterValue, + { withUnit = true, defaultValue = 'N/A' }: FormatterOptions = {} +) { + if (value == null) { + return defaultValue; + } + const formatter = getTimeFormatter(value); + return formatter(value, { withUnit, defaultValue }); } -export function asDecimal(value: number): string { +export function asDecimal(value: number) { return numeral(value).format('0,0.0'); } -export function asInteger(value: number): string { +export function asInteger(value: number) { return numeral(value).format('0,0'); } -export function tpmUnit(type: string): string { +export function tpmUnit(type: string) { return type === 'request' ? 'rpm' : 'tpm'; } diff --git a/x-pack/plugins/apm/typings/numeral.d.ts b/x-pack/plugins/apm/typings/numeral.d.ts new file mode 100644 index 0000000000000..d21595041f14d --- /dev/null +++ b/x-pack/plugins/apm/typings/numeral.d.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +interface Numeral { + (value?: any): Numeral; + format: (pattern: string) => string; +} + +declare var numeral: Numeral; + +declare module '@elastic/numeral' { + export = numeral; +}