[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:
Cauê Marcondes 2019-11-15 13:26:57 +01:00 committed by GitHub
parent 0c42aa8a58
commit c982b8f4dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 890 additions and 511 deletions

View file

@ -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}

View file

@ -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
)

View file

@ -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',

View file

@ -128,7 +128,7 @@ export function AgentConfigurationList({
),
sortable: true,
render: (value: number) => (
<TimestampTooltip time={value} precision="minutes" />
<TimestampTooltip time={value} timeUnit="minutes" />
)
},
{

View file

@ -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',

View file

@ -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(

View file

@ -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>
);
}

View file

@ -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',

View file

@ -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>
&nbsp;
<EuiText size="s">

View file

@ -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)

View file

@ -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)');
});
});

View file

@ -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)');
});
});

View file

@ -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}>

View file

@ -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 %');
};

View file

@ -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}
/>
);

View file

@ -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) =>

View file

@ -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>
}
>

View file

@ -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}
/>
)}

View file

@ -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 ? (

View file

@ -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>
(

View file

@ -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 (
<>

View file

@ -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
},

View file

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

View file

@ -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);
});

View file

@ -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)'
);
});
});
});

View file

@ -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);
});
});
});

View file

@ -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');
});
});
});

View file

@ -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;

View 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}`;
}

View 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;
}

View file

@ -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%');
}

View 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';

View 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;
};

View file

@ -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;

View file

@ -27,3 +27,5 @@ export type PromiseReturnType<Func> = Func extends (
) => Promise<infer Value>
? Value
: Func;
export type Maybe<T> = T | null | undefined;

View file

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