Skip to content

Commit

Permalink
[APM] Transition to Elastic charts for all relevant APM charts (#80298)
Browse files Browse the repository at this point in the history
* adding elastic charts

* fixing some stuff

* refactoring

* fixing ts issues

* fixing unit test

* fix i18n

* adding isLoading prop

* adding annotations toggle, replacing transaction error rate to elastic chart

* adding loading state

* adding empty message

* fixing i18n

* removing unused files

* fixing i18n

* removing e2e test since elastic charts uses canvas

* addressing pr comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
cauemarcondes and kibanamachine authored Nov 9, 2020
1 parent c78cf35 commit 0217073
Show file tree
Hide file tree
Showing 33 changed files with 718 additions and 2,742 deletions.
3 changes: 1 addition & 2 deletions x-pack/plugins/apm/e2e/cypress/integration/apm.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ Feature: APM
Scenario: Transaction duration charts
Given a user browses the APM UI application
When the user inspects the opbeans-node service
Then should redirect to correct path with correct params
And should have correct y-axis ticks
Then should redirect to correct path with correct params
13 changes: 0 additions & 13 deletions x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,3 @@ Then(`should redirect to correct path with correct params`, () => {
cy.url().should('contain', `/app/apm/services/opbeans-node/transactions`);
cy.url().should('contain', `transactionType=request`);
});

Then(`should have correct y-axis ticks`, () => {
const yAxisTick =
'[data-cy=transaction-duration-charts] .rv-xy-plot__axis--vertical .rv-xy-plot__axis__tick__text';

// wait for all loading to finish
cy.get('kbnLoadingIndicator').should('not.be.visible');

// literal assertions because snapshot() doesn't retry
cy.get(yAxisTick).eq(2).should('have.text', '55 ms');
cy.get(yAxisTick).eq(1).should('have.text', '28 ms');
cy.get(yAxisTick).eq(0).should('have.text', '0 ms');
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/

import {
Axis,
Chart,
HistogramBarSeries,
niceTimeFormatter,
Position,
ScaleType,
Settings,
SettingsSpec,
TooltipValue,
} from '@elastic/charts';
import { EuiTitle } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import d3 from 'd3';
import { scaleUtc } from 'd3-scale';
import { mean } from 'lodash';
import React from 'react';
import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters';
import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs';
// @ts-expect-error
import Histogram from '../../../shared/charts/Histogram';
import { EmptyMessage } from '../../../shared/EmptyMessage';

interface IBucket {
key: number;
count: number | undefined;
}

// TODO: cleanup duplication of this in distribution/get_distribution.ts (ErrorDistributionAPIResponse) and transactions/distribution/index.ts (TransactionDistributionAPIResponse)
interface IDistribution {
noHits: boolean;
buckets: IBucket[];
bucketSize: number;
}
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import type { ErrorDistributionAPIResponse } from '../../../../../server/lib/errors/distribution/get_distribution';
import { useTheme } from '../../../../hooks/useTheme';

interface FormattedBucket {
x0: number;
Expand All @@ -37,13 +30,9 @@ interface FormattedBucket {
}

export function getFormattedBuckets(
buckets: IBucket[],
buckets: ErrorDistributionAPIResponse['buckets'],
bucketSize: number
): FormattedBucket[] | null {
if (!buckets) {
return null;
}

): FormattedBucket[] {
return buckets.map(({ count, key }) => {
return {
x0: key,
Expand All @@ -54,76 +43,66 @@ export function getFormattedBuckets(
}

interface Props {
distribution: IDistribution;
distribution: ErrorDistributionAPIResponse;
title: React.ReactNode;
}

const tooltipHeader = (bucket: FormattedBucket) =>
asRelativeDateTimeRange(bucket.x0, bucket.x);

export function ErrorDistribution({ distribution, title }: Props) {
const theme = useTheme();
const buckets = getFormattedBuckets(
distribution.buckets,
distribution.bucketSize
);

if (!buckets) {
return (
<EmptyMessage
heading={i18n.translate('xpack.apm.errorGroupDetails.noErrorsLabel', {
defaultMessage: 'No errors were found',
})}
/>
);
}

const averageValue = mean(buckets.map((bucket) => bucket.y)) || 0;
const xMin = d3.min(buckets, (d) => d.x0);
const xMax = d3.max(buckets, (d) => d.x);
const tickFormat = scaleUtc().domain([xMin, xMax]).tickFormat();
const xMax = d3.max(buckets, (d) => d.x0);

const xFormatter = niceTimeFormatter([xMin, xMax]);

const tooltipProps: SettingsSpec['tooltip'] = {
headerFormatter: (tooltip: TooltipValue) => {
const serie = buckets.find((bucket) => bucket.x0 === tooltip.value);
if (serie) {
return asRelativeDateTimeRange(serie.x0, serie.x);
}
return `${tooltip.value}`;
},
};

return (
<div>
<EuiTitle size="xs">
<span>{title}</span>
</EuiTitle>
<Histogram
height={180}
noHits={distribution.noHits}
tooltipHeader={tooltipHeader}
verticalLineHover={(bucket: FormattedBucket) => bucket.x}
xType="time-utc"
formatX={(value: Date) => {
const time = value.getTime();
return tickFormat(new Date(time - getTimezoneOffsetInMs(time)));
}}
buckets={buckets}
bucketSize={distribution.bucketSize}
formatYShort={(value: number) =>
i18n.translate('xpack.apm.errorGroupDetails.occurrencesShortLabel', {
defaultMessage: '{occCount} occ.',
values: { occCount: value },
})
}
formatYLong={(value: number) =>
i18n.translate('xpack.apm.errorGroupDetails.occurrencesLongLabel', {
defaultMessage:
'{occCount} {occCount, plural, one {occurrence} other {occurrences}}',
values: { occCount: value },
})
}
legends={[
{
color: theme.euiColorVis1,
// 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m
legendValue: numeral(averageValue).format('0a'),
title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', {
defaultMessage: 'Avg.',
}),
legendClickDisabled: true,
},
]}
/>
<div style={{ height: 180 }}>
<Chart>
<Settings
xDomain={{ min: xMin, max: xMax }}
tooltip={tooltipProps}
showLegend
showLegendExtra
legendPosition={Position.Bottom}
/>
<Axis
id="x-axis"
position={Position.Bottom}
showOverlappingTicks
tickFormat={xFormatter}
/>
<Axis id="y-axis" position={Position.Left} ticks={2} showGridLines />
<HistogramBarSeries
minBarHeight={2}
id="errorOccurrences"
name="Occurences"
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x0"
yAccessors={['y']}
data={buckets}
color={theme.eui.euiColorVis1}
/>
</Chart>
</div>
</div>
);
}
Loading

0 comments on commit 0217073

Please sign in to comment.