[APM] Update Error occurrences graph tooltip to display start and end for bucket period (#49638)
* Adding end time inside error tooltip * changing end time precision * refactoring * refactoring * pr comments refactoring * pr comments refactoring * pr comments refactoring * pr comments refactoring * renaming some functions to make it more clear * Refactoring date difference range * refactoring transformers file * refactoring date time formatters * refactoring formatters into a new folder * refactoring getDurationUnit * refactoring duration formatter * fixing unit test * refactoring unit test * Adding timezone to tests * fixing translation issue * fixing translation issue * improving code * exporting toMicroseconds * removing unused import * refactoring duration * refactoring duration * fixing unit test * fixing unit test
This commit is contained in:
parent
0c42aa8a58
commit
c982b8f4dc
|
@ -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) {
|
|||
<span>{title}</span>
|
||||
</EuiTitle>
|
||||
<Histogram
|
||||
tooltipHeader={tooltipHeader}
|
||||
verticalLineHover={(bucket: FormattedBucket) => bucket.x}
|
||||
xType="time"
|
||||
buckets={buckets}
|
||||
|
|
|
@ -143,7 +143,7 @@ const ErrorGroupList: React.FC<Props> = props => {
|
|||
align: 'right',
|
||||
render: (value?: number) =>
|
||||
value ? (
|
||||
<TimestampTooltip time={value} precision="minutes" />
|
||||
<TimestampTooltip time={value} timeUnit="minutes" />
|
||||
) : (
|
||||
NOT_AVAILABLE_LABEL
|
||||
)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -128,7 +128,7 @@ export function AgentConfigurationList({
|
|||
),
|
||||
sortable: true,
|
||||
render: (value: number) => (
|
||||
<TimestampTooltip time={value} precision="minutes" />
|
||||
<TimestampTooltip time={value} timeUnit="minutes" />
|
||||
)
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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<ITableColumn<ITransactionGroup>> = [
|
|||
}),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => asMillis(value)
|
||||
render: (time: number) =>
|
||||
convertTo({
|
||||
unit: 'milliseconds',
|
||||
microseconds: time
|
||||
}).formatted
|
||||
},
|
||||
{
|
||||
field: 'transactionsPerMinute',
|
||||
|
|
|
@ -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<Props> = (
|
|||
);
|
||||
|
||||
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<Props> = (
|
|||
});
|
||||
}
|
||||
}}
|
||||
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(
|
||||
|
|
|
@ -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<SpanActionToolTipProps> = ({
|
|||
function Duration({ item }: { item: IWaterfallItem }) {
|
||||
return (
|
||||
<EuiText color="subdued" size="xs">
|
||||
{asTime(item.duration)}
|
||||
{asDuration(item.duration)}
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<ITableColumn<ITransactionGroup>> = 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',
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<EuiToolTip content={label}>
|
||||
<EuiText>{asTime(duration)}</EuiText>
|
||||
<EuiText>{asDuration(duration)}</EuiText>
|
||||
</EuiToolTip>
|
||||
|
||||
<EuiText size="s">
|
||||
|
|
|
@ -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<React.ReactElement | null | undefined>;
|
||||
items: Array<Maybe<React.ReactElement>>;
|
||||
}
|
||||
|
||||
// TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840)
|
||||
|
|
|
@ -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(<TimestampTooltip time={timestamp} />))
|
||||
.toMatchInlineSnapshot(`
|
||||
<EuiToolTip
|
||||
content="Oct 10, 2019, 08:06:40.123 (UTC-7)"
|
||||
delay="regular"
|
||||
position="top"
|
||||
>
|
||||
5 hours ago
|
||||
</EuiToolTip>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should format with precision in milliseconds by default', () => {
|
||||
expect(
|
||||
shallow(<TimestampTooltip time={timestamp} />)
|
||||
.find('EuiToolTip')
|
||||
.prop('content')
|
||||
).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)');
|
||||
});
|
||||
|
||||
it('should format with precision in seconds', () => {
|
||||
expect(
|
||||
shallow(<TimestampTooltip time={timestamp} timeUnit="seconds" />)
|
||||
.find('EuiToolTip')
|
||||
.prop('content')
|
||||
).toBe('Oct 10, 2019, 08:06:40 (UTC-7)');
|
||||
});
|
||||
|
||||
it('should format with precision in minutes', () => {
|
||||
expect(
|
||||
shallow(<TimestampTooltip time={timestamp} timeUnit="minutes" />)
|
||||
.find('EuiToolTip')
|
||||
.prop('content')
|
||||
).toBe('Oct 10, 2019, 08:06 (UTC-7)');
|
||||
});
|
||||
});
|
|
@ -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(<TimestampTooltip time={timestamp} />))
|
||||
.toMatchInlineSnapshot(`
|
||||
<EuiToolTip
|
||||
content="Oct 10, 2019, 08:06:40.123 (UTC-7)"
|
||||
delay="regular"
|
||||
position="top"
|
||||
>
|
||||
5 hours ago
|
||||
</EuiToolTip>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should format with precision in milliseconds by default', () => {
|
||||
expect(
|
||||
shallow(<TimestampTooltip time={timestamp} />)
|
||||
.find('EuiToolTip')
|
||||
.prop('content')
|
||||
).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)');
|
||||
});
|
||||
|
||||
it('should format with precision in seconds', () => {
|
||||
expect(
|
||||
shallow(<TimestampTooltip time={timestamp} precision="seconds" />)
|
||||
.find('EuiToolTip')
|
||||
.prop('content')
|
||||
).toBe('Oct 10, 2019, 08:06:40 (UTC-7)');
|
||||
});
|
||||
|
||||
it('should format with precision in minutes', () => {
|
||||
expect(
|
||||
shallow(<TimestampTooltip time={timestamp} precision="minutes" />)
|
||||
.find('EuiToolTip')
|
||||
.prop('content')
|
||||
).toBe('Oct 10, 2019, 08:06 (UTC-7)');
|
||||
});
|
||||
|
||||
it('should format with precision in days', () => {
|
||||
expect(
|
||||
shallow(<TimestampTooltip time={timestamp} precision="days" />)
|
||||
.find('EuiToolTip')
|
||||
.prop('content')
|
||||
).toBe('Oct 10, 2019 (UTC-7)');
|
||||
});
|
||||
});
|
|
@ -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 (
|
||||
<EuiToolTip content={absoluteTimeLabel}>
|
||||
|
|
|
@ -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<number>) => {
|
||||
return numeral(y || 0).format('0 %');
|
||||
};
|
||||
|
||||
|
|
|
@ -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(
|
||||
<HistogramInner
|
||||
|
@ -34,14 +32,14 @@ describe('Histogram', () => {
|
|||
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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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<number | string>;
|
||||
end: Maybe<number | string>;
|
||||
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<number>) => asPercent(y || 0, 1);
|
||||
}
|
||||
case 'time': {
|
||||
return (y: number | null | undefined) => asTime(y);
|
||||
return (y: Maybe<number>) => asDuration(y);
|
||||
}
|
||||
case 'integer': {
|
||||
return (y: number | null | undefined) =>
|
||||
return (y: Maybe<number>) =>
|
||||
isValidCoordinateValue(y) ? asInteger(y) : y;
|
||||
}
|
||||
default: {
|
||||
return (y: number | null | undefined) =>
|
||||
return (y: Maybe<number>) =>
|
||||
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) =>
|
||||
|
|
|
@ -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={
|
||||
<div>
|
||||
<NameContainer>{agentMark.name}</NameContainer>
|
||||
<TimeContainer>{asTime(agentMark.us)}</TimeContainer>
|
||||
<TimeContainer>{asDuration(agentMark.us)}</TimeContainer>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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 (
|
||||
<Sticky disableCompensation>
|
||||
|
@ -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 && (
|
||||
<LastTickValue
|
||||
x={xScale(topTraceDuration)}
|
||||
value={tickFormat(topTraceDuration)}
|
||||
value={topTraceDurationFormatted}
|
||||
marginTop={28}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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 (
|
||||
<Hint {...props} value={{ x, y }}>
|
||||
<TooltipElm>
|
||||
<Header>
|
||||
{header || asAbsoluteTime({ time: x, precision: 'seconds' })}
|
||||
</Header>
|
||||
<Header>{header || asAbsoluteDateTime(x, 'seconds')}</Header>
|
||||
|
||||
<Content>
|
||||
{showLegends ? (
|
||||
|
|
|
@ -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<{
|
|||
)}
|
||||
</div>
|
||||
<div style={{ fontWeight: 'bold', fontSize: fontSizes.large }}>
|
||||
{asTime(value)}
|
||||
{asDuration(value)}
|
||||
</div>
|
||||
<div>
|
||||
(
|
||||
|
|
|
@ -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<TransactionChartProps> {
|
|||
};
|
||||
|
||||
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<TransactionChartProps> {
|
|||
const { responseTimeSeries, tpmSeries } = charts;
|
||||
const { transactionType } = urlParams;
|
||||
const maxY = this.getMaxY(responseTimeSeries);
|
||||
const formatter = getTimeFormatter(maxY);
|
||||
const formatter = getDurationFormatter(maxY);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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<string, any | any[]> | null | undefined,
|
||||
item: Maybe<Record<string, any | any[]>>,
|
||||
parentKey?: string
|
||||
): KeyValuePair[] => {
|
||||
if (item) {
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
149
x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts
Normal file
149
x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts
Normal file
|
@ -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}`;
|
||||
}
|
153
x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts
Normal file
153
x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts
Normal file
|
@ -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<number>,
|
||||
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<number>;
|
||||
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<number>,
|
||||
{ defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {}
|
||||
) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const formatter = getDurationFormatter(value);
|
||||
return formatter(value, { defaultValue }).formatted;
|
||||
}
|
|
@ -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%');
|
||||
}
|
10
x-pack/legacy/plugins/apm/public/utils/formatters/index.ts
Normal file
10
x-pack/legacy/plugins/apm/public/utils/formatters/index.ts
Normal file
|
@ -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';
|
67
x-pack/legacy/plugins/apm/public/utils/formatters/size.ts
Normal file
67
x-pack/legacy/plugins/apm/public/utils/formatters/size.ts
Normal file
|
@ -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<number>) => {
|
||||
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;
|
||||
};
|
|
@ -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<number>): value is number =>
|
||||
value !== null && value !== undefined;
|
||||
|
|
|
@ -27,3 +27,5 @@ export type PromiseReturnType<Func> = Func extends (
|
|||
) => Promise<infer Value>
|
||||
? Value
|
||||
: Func;
|
||||
|
||||
export type Maybe<T> = T | null | undefined;
|
||||
|
|
|
@ -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<number>;
|
||||
}
|
||||
|
||||
export interface RectCoordinate {
|
||||
|
|
Loading…
Reference in a new issue