[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
|
// @ts-ignore
|
||||||
import Histogram from '../../../shared/charts/Histogram';
|
import Histogram from '../../../shared/charts/Histogram';
|
||||||
import { EmptyMessage } from '../../../shared/EmptyMessage';
|
import { EmptyMessage } from '../../../shared/EmptyMessage';
|
||||||
|
import { asRelativeDateTimeRange } from '../../../../utils/formatters';
|
||||||
|
|
||||||
interface IBucket {
|
interface IBucket {
|
||||||
key: number;
|
key: number;
|
||||||
|
@ -51,6 +52,9 @@ interface Props {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tooltipHeader = (bucket: FormattedBucket) =>
|
||||||
|
asRelativeDateTimeRange(bucket.x0, bucket.x);
|
||||||
|
|
||||||
export function ErrorDistribution({ distribution, title }: Props) {
|
export function ErrorDistribution({ distribution, title }: Props) {
|
||||||
const buckets = getFormattedBuckets(
|
const buckets = getFormattedBuckets(
|
||||||
distribution.buckets,
|
distribution.buckets,
|
||||||
|
@ -73,6 +77,7 @@ export function ErrorDistribution({ distribution, title }: Props) {
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
<Histogram
|
<Histogram
|
||||||
|
tooltipHeader={tooltipHeader}
|
||||||
verticalLineHover={(bucket: FormattedBucket) => bucket.x}
|
verticalLineHover={(bucket: FormattedBucket) => bucket.x}
|
||||||
xType="time"
|
xType="time"
|
||||||
buckets={buckets}
|
buckets={buckets}
|
||||||
|
|
|
@ -143,7 +143,7 @@ const ErrorGroupList: React.FC<Props> = props => {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (value?: number) =>
|
render: (value?: number) =>
|
||||||
value ? (
|
value ? (
|
||||||
<TimestampTooltip time={value} precision="minutes" />
|
<TimestampTooltip time={value} timeUnit="minutes" />
|
||||||
) : (
|
) : (
|
||||||
NOT_AVAILABLE_LABEL
|
NOT_AVAILABLE_LABEL
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import styled from 'styled-components';
|
||||||
import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services';
|
import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services';
|
||||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||||
import { fontSizes, truncate } from '../../../../style/variables';
|
import { fontSizes, truncate } from '../../../../style/variables';
|
||||||
import { asDecimal, asMillis } from '../../../../utils/formatters';
|
import { asDecimal, convertTo } from '../../../../utils/formatters';
|
||||||
import { ManagedTable } from '../../../shared/ManagedTable';
|
import { ManagedTable } from '../../../shared/ManagedTable';
|
||||||
import { EnvironmentBadge } from '../../../shared/EnvironmentBadge';
|
import { EnvironmentBadge } from '../../../shared/EnvironmentBadge';
|
||||||
import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink';
|
import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink';
|
||||||
|
@ -80,7 +80,11 @@ export const SERVICE_COLUMNS = [
|
||||||
}),
|
}),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
dataType: 'number',
|
dataType: 'number',
|
||||||
render: (value: number) => asMillis(value)
|
render: (time: number) =>
|
||||||
|
convertTo({
|
||||||
|
unit: 'milliseconds',
|
||||||
|
microseconds: time
|
||||||
|
}).formatted
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'transactionsPerMinute',
|
field: 'transactionsPerMinute',
|
||||||
|
|
|
@ -128,7 +128,7 @@ export function AgentConfigurationList({
|
||||||
),
|
),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (value: number) => (
|
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 styled from 'styled-components';
|
||||||
import { ITransactionGroup } from '../../../../server/lib/transaction_groups/transform';
|
import { ITransactionGroup } from '../../../../server/lib/transaction_groups/transform';
|
||||||
import { fontSizes, truncate } from '../../../style/variables';
|
import { fontSizes, truncate } from '../../../style/variables';
|
||||||
import { asMillis } from '../../../utils/formatters';
|
import { convertTo } from '../../../utils/formatters';
|
||||||
import { EmptyMessage } from '../../shared/EmptyMessage';
|
import { EmptyMessage } from '../../shared/EmptyMessage';
|
||||||
import { ImpactBar } from '../../shared/ImpactBar';
|
import { ImpactBar } from '../../shared/ImpactBar';
|
||||||
import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink';
|
import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink';
|
||||||
|
@ -66,7 +66,11 @@ const traceListColumns: Array<ITableColumn<ITransactionGroup>> = [
|
||||||
}),
|
}),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
dataType: 'number',
|
dataType: 'number',
|
||||||
render: (value: number) => asMillis(value)
|
render: (time: number) =>
|
||||||
|
convertTo({
|
||||||
|
unit: 'milliseconds',
|
||||||
|
microseconds: time
|
||||||
|
}).formatted
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'transactionsPerMinute',
|
field: 'transactionsPerMinute',
|
||||||
|
|
|
@ -11,7 +11,7 @@ import React, { FunctionComponent, useCallback } from 'react';
|
||||||
import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
|
import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
|
||||||
import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform';
|
import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform';
|
||||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||||
import { getTimeFormatter, timeUnit } from '../../../../utils/formatters';
|
import { getDurationFormatter } from '../../../../utils/formatters';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Histogram from '../../../shared/charts/Histogram';
|
import Histogram from '../../../shared/charts/Histogram';
|
||||||
import { EmptyMessage } from '../../../shared/EmptyMessage';
|
import { EmptyMessage } from '../../../shared/EmptyMessage';
|
||||||
|
@ -132,8 +132,7 @@ export const TransactionDistribution: FunctionComponent<Props> = (
|
||||||
);
|
);
|
||||||
|
|
||||||
const xMax = d3.max(buckets, d => d.x) || 0;
|
const xMax = d3.max(buckets, d => d.x) || 0;
|
||||||
const timeFormatter = getTimeFormatter(xMax);
|
const timeFormatter = getDurationFormatter(xMax);
|
||||||
const unit = timeUnit(xMax);
|
|
||||||
|
|
||||||
const bucketIndex = buckets.findIndex(
|
const bucketIndex = buckets.findIndex(
|
||||||
bucket =>
|
bucket =>
|
||||||
|
@ -187,18 +186,18 @@ export const TransactionDistribution: FunctionComponent<Props> = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
formatX={timeFormatter}
|
formatX={(time: number) => timeFormatter(time).formatted}
|
||||||
formatYShort={formatYShort}
|
formatYShort={formatYShort}
|
||||||
formatYLong={formatYLong}
|
formatYLong={formatYLong}
|
||||||
verticalLineHover={(bucket: IChartPoint) =>
|
verticalLineHover={(bucket: IChartPoint) =>
|
||||||
bucket.y > 0 && !bucket.sample
|
bucket.y > 0 && !bucket.sample
|
||||||
}
|
}
|
||||||
backgroundHover={(bucket: IChartPoint) => bucket.y > 0 && bucket.sample}
|
backgroundHover={(bucket: IChartPoint) => bucket.y > 0 && bucket.sample}
|
||||||
tooltipHeader={(bucket: IChartPoint) =>
|
tooltipHeader={(bucket: IChartPoint) => {
|
||||||
`${timeFormatter(bucket.x0, {
|
const xFormatted = timeFormatter(bucket.x);
|
||||||
withUnit: false
|
const x0Formatted = timeFormatter(bucket.x0);
|
||||||
})} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}`
|
return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
|
||||||
}
|
}}
|
||||||
tooltipFooter={(bucket: IChartPoint) =>
|
tooltipFooter={(bucket: IChartPoint) =>
|
||||||
!bucket.sample &&
|
!bucket.sample &&
|
||||||
i18n.translate(
|
i18n.translate(
|
||||||
|
|
|
@ -12,7 +12,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { isRumAgentName } from '../../../../../../../common/agent_name';
|
import { isRumAgentName } from '../../../../../../../common/agent_name';
|
||||||
import { px, unit, units } from '../../../../../../style/variables';
|
import { px, unit, units } from '../../../../../../style/variables';
|
||||||
import { asTime } from '../../../../../../utils/formatters';
|
import { asDuration } from '../../../../../../utils/formatters';
|
||||||
import { ErrorCountBadge } from '../../ErrorCountBadge';
|
import { ErrorCountBadge } from '../../ErrorCountBadge';
|
||||||
import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers';
|
import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers';
|
||||||
import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink';
|
import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink';
|
||||||
|
@ -133,7 +133,7 @@ const SpanActionToolTip: React.SFC<SpanActionToolTipProps> = ({
|
||||||
function Duration({ item }: { item: IWaterfallItem }) {
|
function Duration({ item }: { item: IWaterfallItem }) {
|
||||||
return (
|
return (
|
||||||
<EuiText color="subdued" size="xs">
|
<EuiText color="subdued" size="xs">
|
||||||
{asTime(item.duration)}
|
{asDuration(item.duration)}
|
||||||
</EuiText>
|
</EuiText>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import styled from 'styled-components';
|
||||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||||
import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform';
|
import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform';
|
||||||
import { fontFamilyCode, truncate } from '../../../../style/variables';
|
import { fontFamilyCode, truncate } from '../../../../style/variables';
|
||||||
import { asDecimal, asMillis } from '../../../../utils/formatters';
|
import { asDecimal, convertTo } from '../../../../utils/formatters';
|
||||||
import { ImpactBar } from '../../../shared/ImpactBar';
|
import { ImpactBar } from '../../../shared/ImpactBar';
|
||||||
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
|
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
|
||||||
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
|
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
|
||||||
|
@ -28,6 +28,12 @@ interface Props {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toMilliseconds = (time: number) =>
|
||||||
|
convertTo({
|
||||||
|
unit: 'milliseconds',
|
||||||
|
microseconds: time
|
||||||
|
}).formatted;
|
||||||
|
|
||||||
export function TransactionList({ items, isLoading }: Props) {
|
export function TransactionList({ items, isLoading }: Props) {
|
||||||
const columns: Array<ITableColumn<ITransactionGroup>> = useMemo(
|
const columns: Array<ITableColumn<ITransactionGroup>> = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
@ -67,7 +73,7 @@ export function TransactionList({ items, isLoading }: Props) {
|
||||||
),
|
),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
dataType: 'number',
|
dataType: 'number',
|
||||||
render: (value: number) => asMillis(value)
|
render: (time: number) => toMilliseconds(time)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'p95',
|
field: 'p95',
|
||||||
|
@ -79,7 +85,7 @@ export function TransactionList({ items, isLoading }: Props) {
|
||||||
),
|
),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
dataType: 'number',
|
dataType: 'number',
|
||||||
render: (value: number) => asMillis(value)
|
render: (time: number) => toMilliseconds(time)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'transactionsPerMinute',
|
field: 'transactionsPerMinute',
|
||||||
|
|
|
@ -7,7 +7,7 @@ import React from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { EuiToolTip, EuiText } from '@elastic/eui';
|
import { EuiToolTip, EuiText } from '@elastic/eui';
|
||||||
import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent';
|
import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent';
|
||||||
import { asTime } from '../../../utils/formatters';
|
import { asDuration } from '../../../utils/formatters';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
duration: number;
|
duration: number;
|
||||||
|
@ -29,7 +29,7 @@ const DurationSummaryItem = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EuiToolTip content={label}>
|
<EuiToolTip content={label}>
|
||||||
<EuiText>{asTime(duration)}</EuiText>
|
<EuiText>{asDuration(duration)}</EuiText>
|
||||||
</EuiToolTip>
|
</EuiToolTip>
|
||||||
|
|
||||||
<EuiText size="s">
|
<EuiText size="s">
|
||||||
|
|
|
@ -8,9 +8,10 @@ import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||||
import { px, units } from '../../../../public/style/variables';
|
import { px, units } from '../../../../public/style/variables';
|
||||||
|
import { Maybe } from '../../../../typings/common';
|
||||||
|
|
||||||
interface Props {
|
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)
|
// 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 React from 'react';
|
||||||
import { EuiToolTip } from '@elastic/eui';
|
import { EuiToolTip } from '@elastic/eui';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
import { asAbsoluteDateTime, TimeUnit } from '../../../utils/formatters';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
* timestamp in milliseconds
|
* timestamp in milliseconds
|
||||||
*/
|
*/
|
||||||
time: number;
|
time: number;
|
||||||
precision?: 'days' | 'minutes' | 'seconds' | 'milliseconds';
|
timeUnit?: TimeUnit;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreciseTime(precision: Props['precision']) {
|
export function TimestampTooltip({ time, timeUnit = 'milliseconds' }: Props) {
|
||||||
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) {
|
|
||||||
const momentTime = moment(time);
|
const momentTime = moment(time);
|
||||||
const relativeTimeLabel = momentTime.fromNow();
|
const relativeTimeLabel = momentTime.fromNow();
|
||||||
const absoluteTimeLabel = asAbsoluteTime({ time, precision });
|
const absoluteTimeLabel = asAbsoluteDateTime(time, timeUnit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiToolTip content={absoluteTimeLabel}>
|
<EuiToolTip content={absoluteTimeLabel}>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import numeral from '@elastic/numeral';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||||
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
|
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
|
||||||
|
import { Maybe } from '../../../../../typings/common';
|
||||||
import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart';
|
import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart';
|
||||||
import { asPercent } from '../../../../utils/formatters';
|
import { asPercent } from '../../../../utils/formatters';
|
||||||
import { unit } from '../../../../style/variables';
|
import { unit } from '../../../../style/variables';
|
||||||
|
@ -19,7 +20,7 @@ interface Props {
|
||||||
timeseries: TimeSeries[];
|
timeseries: TimeSeries[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const tickFormatY = (y: number | null | undefined) => {
|
const tickFormatY = (y: Maybe<number>) => {
|
||||||
return numeral(y || 0).format('0 %');
|
return numeral(y || 0).format('0 %');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,8 @@ import d3 from 'd3';
|
||||||
import { HistogramInner } from '../index';
|
import { HistogramInner } from '../index';
|
||||||
import response from './response.json';
|
import response from './response.json';
|
||||||
import {
|
import {
|
||||||
getTimeFormatter,
|
|
||||||
asDecimal,
|
asDecimal,
|
||||||
timeUnit
|
getDurationFormatter
|
||||||
} from '../../../../../utils/formatters';
|
} from '../../../../../utils/formatters';
|
||||||
import { toJson } from '../../../../../utils/testHelpers';
|
import { toJson } from '../../../../../utils/testHelpers';
|
||||||
import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index';
|
import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index';
|
||||||
|
@ -25,8 +24,7 @@ describe('Histogram', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const buckets = getFormattedBuckets(response.buckets, response.bucketSize);
|
const buckets = getFormattedBuckets(response.buckets, response.bucketSize);
|
||||||
const xMax = d3.max(buckets, d => d.x);
|
const xMax = d3.max(buckets, d => d.x);
|
||||||
const timeFormatter = getTimeFormatter(xMax);
|
const timeFormatter = getDurationFormatter(xMax);
|
||||||
const unit = timeUnit(xMax);
|
|
||||||
|
|
||||||
wrapper = mount(
|
wrapper = mount(
|
||||||
<HistogramInner
|
<HistogramInner
|
||||||
|
@ -34,14 +32,14 @@ describe('Histogram', () => {
|
||||||
bucketSize={response.bucketSize}
|
bucketSize={response.bucketSize}
|
||||||
transactionId="myTransactionId"
|
transactionId="myTransactionId"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
formatX={timeFormatter}
|
formatX={time => timeFormatter(time).formatted}
|
||||||
formatYShort={t => `${asDecimal(t)} occ.`}
|
formatYShort={t => `${asDecimal(t)} occ.`}
|
||||||
formatYLong={t => `${asDecimal(t)} occurrences`}
|
formatYLong={t => `${asDecimal(t)} occurrences`}
|
||||||
tooltipHeader={bucket =>
|
tooltipHeader={bucket => {
|
||||||
`${timeFormatter(bucket.x0, {
|
const xFormatted = timeFormatter(bucket.x);
|
||||||
withUnit: false
|
const x0Formatted = timeFormatter(bucket.x0);
|
||||||
})} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}`
|
return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
|
||||||
}
|
}}
|
||||||
width={800}
|
width={800}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,20 +9,21 @@ import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import CustomPlot from '../CustomPlot';
|
import CustomPlot from '../CustomPlot';
|
||||||
import {
|
import {
|
||||||
asDynamicBytes,
|
|
||||||
asPercent,
|
|
||||||
getFixedByteFormatter,
|
|
||||||
asDecimal,
|
asDecimal,
|
||||||
asTime,
|
asPercent,
|
||||||
asInteger
|
asInteger,
|
||||||
|
asDynamicBytes,
|
||||||
|
getFixedByteFormatter,
|
||||||
|
asDuration
|
||||||
} from '../../../../utils/formatters';
|
} from '../../../../utils/formatters';
|
||||||
import { Coordinate } from '../../../../../typings/timeseries';
|
import { Coordinate } from '../../../../../typings/timeseries';
|
||||||
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
|
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
|
||||||
import { useChartsSync } from '../../../../hooks/useChartsSync';
|
import { useChartsSync } from '../../../../hooks/useChartsSync';
|
||||||
|
import { Maybe } from '../../../../../typings/common';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
start: number | string | undefined;
|
start: Maybe<number | string>;
|
||||||
end: number | string | undefined;
|
end: Maybe<number | string>;
|
||||||
chart: GenericMetricsChart;
|
chart: GenericMetricsChart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,17 +65,17 @@ function getYTickFormatter(chart: GenericMetricsChart) {
|
||||||
return getFixedByteFormatter(max);
|
return getFixedByteFormatter(max);
|
||||||
}
|
}
|
||||||
case 'percent': {
|
case 'percent': {
|
||||||
return (y: number | null | undefined) => asPercent(y || 0, 1);
|
return (y: Maybe<number>) => asPercent(y || 0, 1);
|
||||||
}
|
}
|
||||||
case 'time': {
|
case 'time': {
|
||||||
return (y: number | null | undefined) => asTime(y);
|
return (y: Maybe<number>) => asDuration(y);
|
||||||
}
|
}
|
||||||
case 'integer': {
|
case 'integer': {
|
||||||
return (y: number | null | undefined) =>
|
return (y: Maybe<number>) =>
|
||||||
isValidCoordinateValue(y) ? asInteger(y) : y;
|
isValidCoordinateValue(y) ? asInteger(y) : y;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return (y: number | null | undefined) =>
|
return (y: Maybe<number>) =>
|
||||||
isValidCoordinateValue(y) ? asDecimal(y) : y;
|
isValidCoordinateValue(y) ? asDecimal(y) : y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,7 +90,7 @@ function getTooltipFormatter({ yUnit }: GenericMetricsChart) {
|
||||||
return (c: Coordinate) => asPercent(c.y || 0, 1);
|
return (c: Coordinate) => asPercent(c.y || 0, 1);
|
||||||
}
|
}
|
||||||
case 'time': {
|
case 'time': {
|
||||||
return (c: Coordinate) => asTime(c.y);
|
return (c: Coordinate) => asDuration(c.y);
|
||||||
}
|
}
|
||||||
case 'integer': {
|
case 'integer': {
|
||||||
return (c: Coordinate) =>
|
return (c: Coordinate) =>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { EuiToolTip } from '@elastic/eui';
|
||||||
import Legend from '../Legend';
|
import Legend from '../Legend';
|
||||||
import { units, px } from '../../../../style/variables';
|
import { units, px } from '../../../../style/variables';
|
||||||
import styled from 'styled-components';
|
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';
|
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||||
|
|
||||||
const NameContainer = styled.div`
|
const NameContainer = styled.div`
|
||||||
|
@ -39,7 +39,7 @@ export default function AgentMarker({ agentMark, x }) {
|
||||||
content={
|
content={
|
||||||
<div>
|
<div>
|
||||||
<NameContainer>{agentMark.name}</NameContainer>
|
<NameContainer>{agentMark.name}</NameContainer>
|
||||||
<TimeContainer>{asTime(agentMark.us)}</TimeContainer>
|
<TimeContainer>{asDuration(agentMark.us)}</TimeContainer>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { XYPlot, XAxis } from 'react-vis';
|
||||||
import LastTickValue from './LastTickValue';
|
import LastTickValue from './LastTickValue';
|
||||||
import AgentMarker from './AgentMarker';
|
import AgentMarker from './AgentMarker';
|
||||||
import { px } from '../../../../style/variables';
|
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';
|
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||||
|
|
||||||
// Remove any tick that is too close to topTraceDuration
|
// Remove any tick that is too close to topTraceDuration
|
||||||
|
@ -33,8 +33,9 @@ const getXAxisTickValues = (tickValues, topTraceDuration) => {
|
||||||
|
|
||||||
function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) {
|
function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) {
|
||||||
const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues;
|
const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues;
|
||||||
const tickFormat = getTimeFormatter(xMax);
|
const tickFormatter = getDurationFormatter(xMax);
|
||||||
const xAxisTickValues = getXAxisTickValues(tickValues, topTraceDuration);
|
const xAxisTickValues = getXAxisTickValues(tickValues, topTraceDuration);
|
||||||
|
const topTraceDurationFormatted = tickFormatter(topTraceDuration).formatted;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sticky disableCompensation>
|
<Sticky disableCompensation>
|
||||||
|
@ -66,7 +67,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) {
|
||||||
orientation="top"
|
orientation="top"
|
||||||
tickSize={0}
|
tickSize={0}
|
||||||
tickValues={xAxisTickValues}
|
tickValues={xAxisTickValues}
|
||||||
tickFormat={tickFormat}
|
tickFormat={time => tickFormatter(time).formatted}
|
||||||
tickPadding={20}
|
tickPadding={20}
|
||||||
style={{
|
style={{
|
||||||
text: { fill: theme.euiColorDarkShade }
|
text: { fill: theme.euiColorDarkShade }
|
||||||
|
@ -76,7 +77,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) {
|
||||||
{topTraceDuration > 0 && (
|
{topTraceDuration > 0 && (
|
||||||
<LastTickValue
|
<LastTickValue
|
||||||
x={xScale(topTraceDuration)}
|
x={xScale(topTraceDuration)}
|
||||||
value={tickFormat(topTraceDuration)}
|
value={topTraceDurationFormatted}
|
||||||
marginTop={28}
|
marginTop={28}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
} from '../../../../style/variables';
|
} from '../../../../style/variables';
|
||||||
import Legend from '../Legend';
|
import Legend from '../Legend';
|
||||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||||
import { asAbsoluteTime } from '../../TimestampTooltip';
|
import { asAbsoluteDateTime } from '../../../../utils/formatters';
|
||||||
|
|
||||||
const TooltipElm = styled.div`
|
const TooltipElm = styled.div`
|
||||||
margin: 0 ${px(unit)};
|
margin: 0 ${px(unit)};
|
||||||
|
@ -87,9 +87,7 @@ export default function Tooltip({
|
||||||
return (
|
return (
|
||||||
<Hint {...props} value={{ x, y }}>
|
<Hint {...props} value={{ x, y }}>
|
||||||
<TooltipElm>
|
<TooltipElm>
|
||||||
<Header>
|
<Header>{header || asAbsoluteDateTime(x, 'seconds')}</Header>
|
||||||
{header || asAbsoluteTime({ time: x, precision: 'seconds' })}
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<Content>
|
<Content>
|
||||||
{showLegends ? (
|
{showLegends ? (
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { asTime, asInteger } from '../../../../../utils/formatters';
|
import { asDuration, asInteger } from '../../../../../utils/formatters';
|
||||||
import { fontSizes } from '../../../../../style/variables';
|
import { fontSizes } from '../../../../../style/variables';
|
||||||
|
|
||||||
export const ChoroplethToolTip: React.SFC<{
|
export const ChoroplethToolTip: React.SFC<{
|
||||||
|
@ -26,7 +26,7 @@ export const ChoroplethToolTip: React.SFC<{
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontWeight: 'bold', fontSize: fontSizes.large }}>
|
<div style={{ fontWeight: 'bold', fontSize: fontSizes.large }}>
|
||||||
{asTime(value)}
|
{asDuration(value)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
(
|
(
|
||||||
|
|
|
@ -27,13 +27,13 @@ import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||||
import {
|
import {
|
||||||
asInteger,
|
asInteger,
|
||||||
tpmUnit,
|
tpmUnit,
|
||||||
TimeFormatter
|
TimeFormatter,
|
||||||
|
getDurationFormatter
|
||||||
} from '../../../../utils/formatters';
|
} from '../../../../utils/formatters';
|
||||||
import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
|
import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
|
||||||
import { LicenseContext } from '../../../../context/LicenseContext';
|
import { LicenseContext } from '../../../../context/LicenseContext';
|
||||||
import { TransactionLineChart } from './TransactionLineChart';
|
import { TransactionLineChart } from './TransactionLineChart';
|
||||||
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
|
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
|
||||||
import { getTimeFormatter } from '../../../../utils/formatters';
|
|
||||||
import { DurationByCountryMap } from './DurationByCountryMap';
|
import { DurationByCountryMap } from './DurationByCountryMap';
|
||||||
import {
|
import {
|
||||||
TRANSACTION_PAGE_LOAD,
|
TRANSACTION_PAGE_LOAD,
|
||||||
|
@ -74,12 +74,14 @@ export class TransactionCharts extends Component<TransactionChartProps> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public getResponseTimeTickFormatter = (formatter: TimeFormatter) => {
|
public getResponseTimeTickFormatter = (formatter: TimeFormatter) => {
|
||||||
return (t: number) => formatter(t);
|
return (t: number) => formatter(t).formatted;
|
||||||
};
|
};
|
||||||
|
|
||||||
public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => {
|
public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => {
|
||||||
return (p: Coordinate) => {
|
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 { responseTimeSeries, tpmSeries } = charts;
|
||||||
const { transactionType } = urlParams;
|
const { transactionType } = urlParams;
|
||||||
const maxY = this.getMaxY(responseTimeSeries);
|
const maxY = this.getMaxY(responseTimeSeries);
|
||||||
const formatter = getTimeFormatter(maxY);
|
const formatter = getDurationFormatter(maxY);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
RectCoordinate,
|
RectCoordinate,
|
||||||
TimeSeries
|
TimeSeries
|
||||||
} from '../../typings/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 { IUrlParams } from '../context/UrlParamsContext/types';
|
||||||
import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries';
|
import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries';
|
||||||
import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor';
|
import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor';
|
||||||
|
@ -70,6 +70,10 @@ export function getResponseTimeSeries({
|
||||||
}: TimeSeriesAPIResponse) {
|
}: TimeSeriesAPIResponse) {
|
||||||
const { overallAvgDuration } = apmTimeseries;
|
const { overallAvgDuration } = apmTimeseries;
|
||||||
const { avg, p95, p99 } = apmTimeseries.responseTimes;
|
const { avg, p95, p99 } = apmTimeseries.responseTimes;
|
||||||
|
const formattedDuration = convertTo({
|
||||||
|
unit: 'milliseconds',
|
||||||
|
microseconds: overallAvgDuration
|
||||||
|
}).formatted;
|
||||||
|
|
||||||
const series: TimeSeries[] = [
|
const series: TimeSeries[] = [
|
||||||
{
|
{
|
||||||
|
@ -77,7 +81,7 @@ export function getResponseTimeSeries({
|
||||||
defaultMessage: 'Avg.'
|
defaultMessage: 'Avg.'
|
||||||
}),
|
}),
|
||||||
data: avg,
|
data: avg,
|
||||||
legendValue: asMillis(overallAvgDuration),
|
legendValue: formattedDuration,
|
||||||
type: 'linemark',
|
type: 'linemark',
|
||||||
color: theme.euiColorVis1
|
color: theme.euiColorVis1
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { compact, isObject } from 'lodash';
|
import { compact, isObject } from 'lodash';
|
||||||
|
import { Maybe } from '../../typings/common';
|
||||||
|
|
||||||
export interface KeyValuePair {
|
export interface KeyValuePair {
|
||||||
key: string;
|
key: string;
|
||||||
|
@ -12,7 +13,7 @@ export interface KeyValuePair {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const flattenObject = (
|
export const flattenObject = (
|
||||||
item: Record<string, any | any[]> | null | undefined,
|
item: Maybe<Record<string, any | any[]>>,
|
||||||
parentKey?: string
|
parentKey?: string
|
||||||
): KeyValuePair[] => {
|
): KeyValuePair[] => {
|
||||||
if (item) {
|
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;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
import { getFixedByteFormatter, asDynamicBytes } from '../size';
|
||||||
|
|
||||||
import {
|
describe('size formatters', () => {
|
||||||
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('byte formatting', () => {
|
describe('byte formatting', () => {
|
||||||
const bytes = 10;
|
const bytes = 10;
|
||||||
const kb = 1000 + 1;
|
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;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
import { Maybe } from '../../typings/common';
|
||||||
|
|
||||||
export const isValidCoordinateValue = (
|
export const isValidCoordinateValue = (value: Maybe<number>): value is number =>
|
||||||
value: number | null | undefined
|
value !== null && value !== undefined;
|
||||||
): value is number => value !== null && value !== undefined;
|
|
||||||
|
|
|
@ -27,3 +27,5 @@ export type PromiseReturnType<Func> = Func extends (
|
||||||
) => Promise<infer Value>
|
) => Promise<infer Value>
|
||||||
? Value
|
? Value
|
||||||
: Func;
|
: Func;
|
||||||
|
|
||||||
|
export type Maybe<T> = T | null | undefined;
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
import { Maybe } from '../typings/common';
|
||||||
|
|
||||||
export interface Coordinate {
|
export interface Coordinate {
|
||||||
x: number;
|
x: number;
|
||||||
y: number | null | undefined;
|
y: Maybe<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RectCoordinate {
|
export interface RectCoordinate {
|
||||||
|
|
Loading…
Reference in a new issue