From 0217073b8f555b68a7487c5c52325e462a5232af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Mon, 9 Nov 2020 11:03:07 -0300
Subject: [PATCH] [APM] Transition to Elastic charts for all relevant APM
charts (#80298)
* 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>
---
.../apm/e2e/cypress/integration/apm.feature | 3 +-
.../cypress/support/step_definitions/apm.ts | 13 -
.../ErrorGroupDetails/Distribution/index.tsx | 141 +-
.../TransactionDetails/Distribution/index.tsx | 198 ++-
.../app/TransactionDetails/index.tsx | 9 +-
.../TransactionOverview.test.tsx | 14 +-
.../app/TransactionOverview/index.tsx | 23 +-
.../components/app/service_overview/index.tsx | 16 +-
.../TransactionBreakdownGraph/index.tsx | 143 +-
.../shared/TransactionBreakdown/index.tsx | 8 +-
.../shared/charts/Histogram/SingleRect.js | 29 -
.../Histogram/__test__/Histogram.test.js | 119 --
.../__snapshots__/Histogram.test.js.snap | 1504 -----------------
.../charts/Histogram/__test__/response.json | 106 --
.../shared/charts/Histogram/index.js | 319 ----
.../TransactionLineChart/index.tsx | 70 -
.../shared/charts/TransactionCharts/index.tsx | 133 +-
.../TransactionCharts/use_formatter.test.tsx | 106 +-
.../charts/TransactionCharts/use_formatter.ts | 38 +-
.../shared/charts/annotations/index.tsx | 45 +
.../shared/charts/chart_container.test.tsx | 91 +-
.../shared/charts/chart_container.tsx | 41 +-
.../legacy.tsx | 112 --
.../shared/charts/line_chart/index.tsx | 16 +-
.../index.tsx | 56 +-
.../public/hooks/useTransactionBreakdown.ts | 2 +-
.../hooks/useTransactionDistribution.ts | 2 +-
.../apm/public/hooks/useTransactionList.ts | 4 +-
.../apm/public/hooks/use_annotations.ts | 38 +
.../apm/public/selectors/chartSelectors.ts | 41 +-
.../lib/errors/distribution/get_buckets.ts | 2 +-
.../translations/translations/ja-JP.json | 9 -
.../translations/translations/zh-CN.json | 9 -
33 files changed, 718 insertions(+), 2742 deletions(-)
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
create mode 100644 x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
rename x-pack/plugins/apm/public/components/shared/charts/{erroneous_transactions_rate_chart => transaction_error_rate_chart}/index.tsx (64%)
create mode 100644 x-pack/plugins/apm/public/hooks/use_annotations.ts
diff --git a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature
index 285615108266b..494a6b5fadb5b 100644
--- a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature
+++ b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature
@@ -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
\ No newline at end of file
diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
index 50c620dca9ddf..42c2bc7ffd318 100644
--- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
+++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
@@ -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');
-});
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
index e17dd9a9eb038..a17bf7e93e466 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
@@ -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;
@@ -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,
@@ -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 (
-
- );
- }
-
- 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 (
{title}
-
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,
- },
- ]}
- />
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
index 67125d41635a9..bf1bda793179f 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
@@ -4,22 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import {
+ Axis,
+ Chart,
+ ElementClickListener,
+ GeometryValue,
+ HistogramBarSeries,
+ Position,
+ RectAnnotation,
+ ScaleType,
+ Settings,
+ SettingsSpec,
+ TooltipValue,
+ XYChartSeriesIdentifier,
+} from '@elastic/charts';
import { EuiIconTip, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import d3 from 'd3';
import { isEmpty } from 'lodash';
import React, { useCallback } from 'react';
import { ValuesType } from 'utility-types';
+import { useTheme } from '../../../../../../observability/public';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
+import type { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets';
+import type { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
-// @ts-expect-error
-import Histogram from '../../../shared/charts/Histogram';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
+import { unit } from '../../../../style/variables';
+import { ChartContainer } from '../../../shared/charts/chart_container';
import { EmptyMessage } from '../../../shared/EmptyMessage';
-import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
interface IChartPoint {
x0: number;
@@ -31,10 +46,10 @@ interface IChartPoint {
}
export function getFormattedBuckets(
- buckets: DistributionBucket[],
- bucketSize: number
+ buckets?: DistributionBucket[],
+ bucketSize?: number
) {
- if (!buckets) {
+ if (!buckets || !bucketSize) {
return [];
}
@@ -74,7 +89,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
'xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel',
{
defaultMessage:
- '{transCount, plural, =0 {# request} one {# request} other {# requests}}',
+ '{transCount, plural, =0 {request} one {request} other {requests}}',
values: {
transCount: t,
},
@@ -84,7 +99,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel',
{
defaultMessage:
- '{transCount, plural, =0 {# transaction} one {# transaction} other {# transactions}}',
+ '{transCount, plural, =0 {transaction} one {transaction} other {transactions}}',
values: {
transCount: t,
},
@@ -95,21 +110,21 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
interface Props {
distribution?: TransactionDistributionAPIResponse;
urlParams: IUrlParams;
- isLoading: boolean;
+ fetchStatus: FETCH_STATUS;
bucketIndex: number;
onBucketClick: (
bucket: ValuesType
) => void;
}
-export function TransactionDistribution(props: Props) {
- const {
- distribution,
- urlParams: { transactionType },
- isLoading,
- bucketIndex,
- onBucketClick,
- } = props;
+export function TransactionDistribution({
+ distribution,
+ urlParams: { transactionType },
+ fetchStatus,
+ bucketIndex,
+ onBucketClick,
+}: Props) {
+ const theme = useTheme();
/* eslint-disable-next-line react-hooks/exhaustive-deps */
const formatYShort = useCallback(getFormatYShort(transactionType), [
@@ -122,12 +137,10 @@ export function TransactionDistribution(props: Props) {
]);
// no data in response
- if (!distribution || distribution.noHits) {
- // only show loading state if there is no data - else show stale data until new data has loaded
- if (isLoading) {
- return ;
- }
-
+ if (
+ (!distribution || distribution.noHits) &&
+ fetchStatus !== FETCH_STATUS.LOADING
+ ) {
return (
{
- return bucket.key === chartPoint.x0;
- });
-
- return clickedBucket;
- }
-
const buckets = getFormattedBuckets(
- distribution.buckets,
- distribution.bucketSize
+ distribution?.buckets,
+ distribution?.bucketSize
);
- const xMax = d3.max(buckets, (d) => d.x) || 0;
+ const xMin = d3.min(buckets, (d) => d.x0) || 0;
+ const xMax = d3.max(buckets, (d) => d.x0) || 0;
const timeFormatter = getDurationFormatter(xMax);
+ const tooltipProps: SettingsSpec['tooltip'] = {
+ headerFormatter: (tooltip: TooltipValue) => {
+ const serie = buckets.find((bucket) => bucket.x0 === tooltip.value);
+ if (serie) {
+ const xFormatted = timeFormatter(serie.x);
+ const x0Formatted = timeFormatter(serie.x0);
+ return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
+ }
+ return `${timeFormatter(tooltip.value)}`;
+ },
+ };
+
+ const onBarClick: ElementClickListener = (elements) => {
+ const chartPoint = elements[0][0] as GeometryValue;
+ const clickedBucket = distribution?.buckets.find((bucket) => {
+ return bucket.key === chartPoint.x;
+ });
+ if (clickedBucket) {
+ onBucketClick(clickedBucket);
+ }
+ };
+
+ const selectedBucket = buckets[bucketIndex];
+
return (
@@ -181,42 +211,66 @@ export function TransactionDistribution(props: Props) {
/>
-
-
{
- const clickedBucket = getBucketFromChartPoint(chartPoint);
-
- if (clickedBucket) {
- onBucketClick(clickedBucket);
- }
- }}
- formatX={(time: number) => timeFormatter(time).formatted}
- formatYShort={formatYShort}
- formatYLong={formatYLong}
- verticalLineHover={(point: IChartPoint) =>
- isEmpty(getBucketFromChartPoint(point)?.samples)
- }
- backgroundHover={(point: IChartPoint) =>
- !isEmpty(getBucketFromChartPoint(point)?.samples)
- }
- tooltipHeader={(point: IChartPoint) => {
- const xFormatted = timeFormatter(point.x);
- const x0Formatted = timeFormatter(point.x0);
- return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
- }}
- tooltipFooter={(point: IChartPoint) =>
- isEmpty(getBucketFromChartPoint(point)?.samples) &&
- i18n.translate(
- 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip',
- {
- defaultMessage: 'No sample available for this bucket',
- }
- )
- }
- />
+
+
+
+ {selectedBucket && (
+
+ )}
+ timeFormatter(time).formatted}
+ />
+ formatYShort(value)}
+ />
+ value}
+ minBarHeight={2}
+ id="transactionDurationDistribution"
+ name={(series: XYChartSeriesIdentifier) => {
+ const bucketCount = series.splitAccessors.get(
+ series.yAccessor
+ ) as number;
+ return formatYLong(bucketCount);
+ }}
+ splitSeriesAccessors={['y']}
+ xScaleType={ScaleType.Linear}
+ yScaleType={ScaleType.Linear}
+ xAccessor="x0"
+ yAccessors={['y']}
+ data={buckets}
+ color={theme.eui.euiColorVis1}
+ />
+
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
index efdd7b1f34221..e4c36b028e55c 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
@@ -52,7 +52,11 @@ export function TransactionDetails({
status: distributionStatus,
} = useTransactionDistribution(urlParams);
- const { data: transactionChartsData } = useTransactionCharts();
+ const {
+ data: transactionChartsData,
+ status: transactionChartsStatus,
+ } = useTransactionCharts();
+
const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall(
urlParams
);
@@ -121,6 +125,7 @@ export function TransactionDetails({
@@ -131,7 +136,7 @@ export function TransactionDetails({
{
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
index b7d1b93600a73..c530a7e1489ad 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
@@ -4,12 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- fireEvent,
- getByText,
- queryByLabelText,
- render,
-} from '@testing-library/react';
+import { fireEvent, getByText, queryByLabelText } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { CoreStart } from 'kibana/public';
import React from 'react';
@@ -20,7 +15,10 @@ import { UrlParamsProvider } from '../../../context/UrlParamsContext';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import * as useFetcherHook from '../../../hooks/useFetcher';
import * as useServiceTransactionTypesHook from '../../../hooks/useServiceTransactionTypes';
-import { disableConsoleWarning } from '../../../utils/testHelpers';
+import {
+ disableConsoleWarning,
+ renderWithTheme,
+} from '../../../utils/testHelpers';
import { fromQuery } from '../../shared/Links/url_helpers';
import { TransactionOverview } from './';
@@ -54,7 +52,7 @@ function setup({
jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any);
- return render(
+ return renderWithTheme(
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
index 5444d2d521f37..df9e673ed4847 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
@@ -22,7 +22,7 @@ import React, { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
-import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context';
+import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';
@@ -33,11 +33,10 @@ import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter';
+import { Correlations } from '../Correlations';
import { TransactionList } from './TransactionList';
import { useRedirect } from './useRedirect';
-import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
import { UserExperienceCallout } from './user_experience_callout';
-import { Correlations } from '../Correlations';
function getRedirectLocation({
urlParams,
@@ -83,7 +82,10 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
})
);
- const { data: transactionCharts } = useTransactionCharts();
+ const {
+ data: transactionCharts,
+ status: transactionChartsStatus,
+ } = useTransactionCharts();
useTrackPageview({ app: 'apm', path: 'transaction_overview' });
useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 });
@@ -135,12 +137,11 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
>
)}
-
-
-
+
@@ -190,7 +191,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
index 342152b572f1e..016ee3daf6b51 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
@@ -11,7 +11,7 @@ import styled from 'styled-components';
import { useTrackPageview } from '../../../../../observability/public';
import { isRumAgentName } from '../../../../common/agent_name';
import { ChartsSyncContextProvider } from '../../../context/charts_sync_context';
-import { ErroneousTransactionsRateChart } from '../../shared/charts/erroneous_transactions_rate_chart';
+import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart';
import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink';
import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink';
import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink';
@@ -125,19 +125,7 @@ export function ServiceOverview({
{!isRumAgentName(agentName) && (
-
-
-
- {i18n.translate(
- 'xpack.apm.serviceOverview.errorRateChartTitle',
- {
- defaultMessage: 'Error rate',
- }
- )}
-
-
-
-
+
)}
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx
index b908eb8da4d03..05cae589c19fc 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx
@@ -4,62 +4,113 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { throttle } from 'lodash';
-import React, { useMemo } from 'react';
+import {
+ AreaSeries,
+ Axis,
+ Chart,
+ niceTimeFormatter,
+ Placement,
+ Position,
+ ScaleType,
+ Settings,
+} from '@elastic/charts';
+import moment from 'moment';
+import React, { useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
import { asPercent } from '../../../../../common/utils/formatters';
-import { useUiTracker } from '../../../../../../observability/public';
-import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
-import { Maybe } from '../../../../../typings/common';
-import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
+import { TimeSeries } from '../../../../../typings/timeseries';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
-import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
-import { getEmptySeries } from '../../charts/CustomPlot/getEmptySeries';
-import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart';
+import { useChartsSync as useChartsSync2 } from '../../../../hooks/use_charts_sync';
+import { unit } from '../../../../style/variables';
+import { Annotations } from '../../charts/annotations';
+import { ChartContainer } from '../../charts/chart_container';
+import { onBrushEnd } from '../../charts/helper/helper';
+
+const XY_HEIGHT = unit * 16;
interface Props {
- timeseries: TimeSeries[];
- noHits: boolean;
+ fetchStatus: FETCH_STATUS;
+ timeseries?: TimeSeries[];
}
-const tickFormatY = (y: Maybe) => {
- return asPercent(y ?? 0, 1);
-};
+export function TransactionBreakdownGraph({ fetchStatus, timeseries }: Props) {
+ const history = useHistory();
+ const chartRef = React.createRef();
+ const { event, setEvent } = useChartsSync2();
+ const { urlParams } = useUrlParams();
+ const { start, end } = urlParams;
-const formatTooltipValue = (coordinate: Coordinate) => {
- return isValidCoordinateValue(coordinate.y)
- ? asPercent(coordinate.y, 1)
- : NOT_AVAILABLE_LABEL;
-};
+ useEffect(() => {
+ if (event.chartId !== 'timeSpentBySpan' && chartRef.current) {
+ chartRef.current.dispatchExternalPointerEvent(event);
+ }
+ }, [chartRef, event]);
-function TransactionBreakdownGraph({ timeseries, noHits }: Props) {
- const { urlParams } = useUrlParams();
- const { rangeFrom, rangeTo } = urlParams;
- const trackApmEvent = useUiTracker({ app: 'apm' });
- const handleHover = useMemo(
- () =>
- throttle(() => trackApmEvent({ metric: 'hover_breakdown_chart' }), 60000),
- [trackApmEvent]
- );
+ const min = moment.utc(start).valueOf();
+ const max = moment.utc(end).valueOf();
- const emptySeries =
- rangeFrom && rangeTo
- ? getEmptySeries(
- new Date(rangeFrom).getTime(),
- new Date(rangeTo).getTime()
- )
- : [];
+ const xFormatter = niceTimeFormatter([min, max]);
return (
-
+
+
+ onBrushEnd({ x, history })}
+ showLegend
+ showLegendExtra
+ legendPosition={Position.Bottom}
+ xDomain={{ min, max }}
+ flatLegend
+ onPointerUpdate={(currEvent: any) => {
+ setEvent(currEvent);
+ }}
+ externalPointerEvents={{
+ tooltip: { visible: true, placement: Placement.Bottom },
+ }}
+ />
+
+ asPercent(y ?? 0, 1)}
+ />
+
+
+
+ {timeseries?.length ? (
+ timeseries.map((serie) => {
+ return (
+
+ );
+ })
+ ) : (
+ // When timeseries is empty, loads an AreaSeries chart to show the default empty message.
+
+ )}
+
+
);
}
-
-export { TransactionBreakdownGraph };
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
index 55826497ca385..9b0c041aaf7b5 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
@@ -5,16 +5,13 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { isEmpty } from 'lodash';
import React from 'react';
-import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown';
import { TransactionBreakdownGraph } from './TransactionBreakdownGraph';
function TransactionBreakdown() {
const { data, status } = useTransactionBreakdown();
const { timeseries } = data;
- const noHits = isEmpty(timeseries) && status === FETCH_STATUS.SUCCESS;
return (
@@ -29,7 +26,10 @@ function TransactionBreakdown() {
-
+
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js
deleted file mode 100644
index ca85ee961f5d8..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js
+++ /dev/null
@@ -1,29 +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 React from 'react';
-import PropTypes from 'prop-types';
-
-function SingleRect({ innerHeight, marginTop, style, x, width }) {
- return (
-
- );
-}
-
-SingleRect.requiresSVG = true;
-SingleRect.propTypes = {
- x: PropTypes.number.isRequired,
-};
-
-export default SingleRect;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
deleted file mode 100644
index 03fd039a3401e..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
+++ /dev/null
@@ -1,119 +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 React from 'react';
-
-import d3 from 'd3';
-import { HistogramInner } from '../index';
-import response from './response.json';
-import {
- disableConsoleWarning,
- toJson,
- mountWithTheme,
-} from '../../../../../utils/testHelpers';
-import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index';
-import {
- asInteger,
- getDurationFormatter,
-} from '../../../../../../common/utils/formatters';
-
-describe('Histogram', () => {
- let mockConsole;
- let wrapper;
-
- const onClick = jest.fn();
-
- beforeAll(() => {
- mockConsole = disableConsoleWarning('Warning: componentWillReceiveProps');
- });
-
- afterAll(() => {
- mockConsole.mockRestore();
- });
-
- beforeEach(() => {
- const buckets = getFormattedBuckets(response.buckets, response.bucketSize);
- const xMax = d3.max(buckets, (d) => d.x);
- const timeFormatter = getDurationFormatter(xMax);
-
- wrapper = mountWithTheme(
- timeFormatter(time).formatted}
- formatYShort={(t) => `${asInteger(t)} occ.`}
- formatYLong={(t) => `${asInteger(t)} occurrences`}
- tooltipHeader={(bucket) => {
- const xFormatted = timeFormatter(bucket.x);
- const x0Formatted = timeFormatter(bucket.x0);
- return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
- }}
- width={800}
- />
- );
- });
-
- describe('Initially', () => {
- it('should have default markup', () => {
- expect(toJson(wrapper)).toMatchSnapshot();
- });
-
- it('should not show tooltip', () => {
- expect(wrapper.find('Tooltip').length).toBe(0);
- });
- });
-
- describe('when hovering over an empty bucket', () => {
- beforeEach(() => {
- wrapper.find('.rv-voronoi__cell').at(2).simulate('mouseOver');
- });
-
- it('should not display tooltip', () => {
- expect(wrapper.find('Tooltip').length).toBe(0);
- });
- });
-
- describe('when hovering over a non-empty bucket', () => {
- beforeEach(() => {
- wrapper.find('.rv-voronoi__cell').at(7).simulate('mouseOver');
- });
-
- it('should display tooltip', () => {
- const tooltips = wrapper.find('Tooltip');
-
- expect(tooltips.length).toBe(1);
- expect(tooltips.prop('header')).toBe('811 - 927 ms');
- expect(tooltips.prop('tooltipPoints')).toEqual([
- { value: '49 occurrences' },
- ]);
- expect(tooltips.prop('x')).toEqual(869010);
- expect(tooltips.prop('y')).toEqual(27.5);
- });
-
- it('should have correct markup for tooltip', () => {
- const tooltips = wrapper.find('Tooltip');
- expect(toJson(tooltips)).toMatchSnapshot();
- });
- });
-
- describe('when clicking on a non-empty bucket', () => {
- beforeEach(() => {
- wrapper.find('.rv-voronoi__cell').at(7).simulate('click');
- });
-
- it('should call onClick with bucket', () => {
- expect(onClick).toHaveBeenCalledWith({
- style: { cursor: 'pointer' },
- xCenter: 869010,
- x0: 811076,
- x: 926944,
- y: 49,
- });
- });
- });
-});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
deleted file mode 100644
index a31b9735628ab..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
+++ /dev/null
@@ -1,1504 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Histogram Initially should have default markup 1`] = `
-.c0 {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- position: absolute;
- top: 0;
- left: 0;
-}
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Histogram when hovering over a non-empty bucket should have correct markup for tooltip 1`] = `
-.c0 {
- margin: 0 16px;
- -webkit-transform: translateY(-50%);
- -ms-transform: translateY(-50%);
- transform: translateY(-50%);
- border: 1px solid #d3dae6;
- background: #ffffff;
- border-radius: 4px;
- font-size: 14px;
- color: #000000;
-}
-
-.c1 {
- background: #f5f7fa;
- border-bottom: 1px solid #d3dae6;
- border-radius: 4px 4px 0 0;
- padding: 8px;
- color: #98a2b3;
-}
-
-.c2 {
- margin: 8px;
- margin-right: 16px;
- font-size: 12px;
-}
-
-.c4 {
- color: #98a2b3;
- margin: 8px;
- font-size: 12px;
-}
-
-.c3 {
- color: #69707d;
- font-size: 14px;
-}
-
-
-
-
-
-
- 811 - 927 ms
-
-
-
-
-
-
- 49 occurrences
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json
deleted file mode 100644
index 302e105dfa997..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "buckets": [
- { "key": 0, "count": 0 },
- { "key": 115868, "count": 0 },
- { "key": 231736, "count": 0 },
- { "key": 347604, "count": 0 },
- { "key": 463472, "count": 0 },
- {
- "key": 579340,
- "count": 8,
- "samples": [
- {
- "transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb"
- }
- ]
- },
- {
- "key": 695208,
- "count": 23,
- "samples": [
- {
- "transactionId": "d327611b-e999-4942-a94f-c60208940180"
- }
- ]
- },
- {
- "key": 811076,
- "count": 49,
- "samples": [
- {
- "transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192"
- }
- ]
- },
- {
- "key": 926944,
- "count": 51,
- "transactionId": "9706a1ec-23f5-4ce8-97e8-69ce35fb0a9a"
- },
- {
- "key": 1042812,
- "count": 46,
- "transactionId": "f8d360c3-dd5e-47b6-b082-9e0bf821d3b2"
- },
- {
- "key": 1158680,
- "count": 13,
- "samples": [
- {
- "transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2"
- }
- ]
- },
- {
- "key": 1274548,
- "count": 7,
- "transactionId": "54b4b5a7-f065-4cab-9016-534e58f4fc0a"
- },
- {
- "key": 1390416,
- "count": 4,
- "transactionId": "8cfac2a3-38e7-4d3a-9792-d008b4bcb867"
- },
- {
- "key": 1506284,
- "count": 3,
- "transactionId": "ce3f3bd3-a37c-419e-bb9c-5db956ded149"
- },
- { "key": 1622152, "count": 0 },
- {
- "key": 1738020,
- "count": 4,
- "transactionId": "2300174b-85d8-40ba-a6cb-eeba2a49debf"
- },
- { "key": 1853888, "count": 0 },
- { "key": 1969756, "count": 0 },
- {
- "key": 2085624,
- "count": 1,
- "transactionId": "774955a4-2ba3-4461-81a6-65759db4805d"
- },
- { "key": 2201492, "count": 0 },
- { "key": 2317360, "count": 0 },
- { "key": 2433228, "count": 0 },
- { "key": 2549096, "count": 0 },
- { "key": 2664964, "count": 0 },
- {
- "key": 2780832,
- "count": 1,
- "transactionId": "035d1b9d-af71-46cf-8910-57bd4faf412d"
- },
- {
- "key": 2896700,
- "count": 1,
- "transactionId": "4a845b32-9de4-4796-8ef4-d7bbdedc9099"
- },
- { "key": 3012568, "count": 0 },
- {
- "key": 3128436,
- "count": 1,
- "transactionId": "68620ffb-7a1b-4f8e-b9bb-009fa5b092be"
- }
- ],
- "bucketSize": 115868,
- "defaultBucketIndex": 12
-}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
deleted file mode 100644
index 3b2109d68c613..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
+++ /dev/null
@@ -1,319 +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 React, { PureComponent } from 'react';
-import d3 from 'd3';
-import { isEmpty } from 'lodash';
-import PropTypes from 'prop-types';
-import { scaleLinear } from 'd3-scale';
-import styled from 'styled-components';
-import SingleRect from './SingleRect';
-import {
- XYPlot,
- XAxis,
- YAxis,
- HorizontalGridLines,
- VerticalRectSeries,
- Voronoi,
- makeWidthFlexible,
- VerticalGridLines,
-} from 'react-vis';
-import { unit } from '../../../../style/variables';
-import Tooltip from '../Tooltip';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
-import { tint } from 'polished';
-import { getTimeTicksTZ, getDomainTZ } from '../helper/timezone';
-import Legends from '../CustomPlot/Legends';
-import StatusText from '../CustomPlot/StatusText';
-import { i18n } from '@kbn/i18n';
-import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
-
-const XY_HEIGHT = unit * 10;
-const XY_MARGIN = {
- top: unit,
- left: unit * 5,
- right: unit,
- bottom: unit * 2,
-};
-
-const X_TICK_TOTAL = 8;
-
-// position absolutely to make sure that window resizing/zooming works
-const ChartsWrapper = styled.div`
- user-select: none;
- position: absolute;
- top: 0;
- left: 0;
-`;
-
-export class HistogramInner extends PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- hoveredBucket: {},
- };
- }
-
- onClick = (bucket) => {
- if (this.props.onClick) {
- this.props.onClick(bucket);
- }
- };
-
- onHover = (bucket) => {
- this.setState({ hoveredBucket: bucket });
- };
-
- onBlur = () => {
- this.setState({ hoveredBucket: {} });
- };
-
- getChartData(items, selectedItem) {
- const yMax = d3.max(items, (d) => d.y);
- const MINIMUM_BUCKET_SIZE = yMax * 0.02;
-
- return items.map((item) => {
- const padding = (item.x - item.x0) / 20;
- return {
- ...item,
- color:
- item === selectedItem
- ? theme.euiColorVis1
- : tint(0.5, theme.euiColorVis1),
- x0: item.x0 + padding,
- x: item.x - padding,
- y: item.y > 0 ? Math.max(item.y, MINIMUM_BUCKET_SIZE) : 0,
- };
- });
- }
-
- render() {
- const {
- backgroundHover,
- bucketIndex,
- buckets,
- bucketSize,
- formatX,
- formatYShort,
- formatYLong,
- tooltipFooter,
- tooltipHeader,
- verticalLineHover,
- width: XY_WIDTH,
- height,
- legends,
- } = this.props;
- const { hoveredBucket } = this.state;
- if (isEmpty(buckets) || XY_WIDTH === 0) {
- return null;
- }
-
- const isTimeSeries =
- this.props.xType === 'time' || this.props.xType === 'time-utc';
-
- const xMin = d3.min(buckets, (d) => d.x0);
- const xMax = d3.max(buckets, (d) => d.x);
- const yMin = 0;
- const yMax = d3.max(buckets, (d) => d.y);
- const selectedBucket = buckets[bucketIndex];
- const chartData = this.getChartData(buckets, selectedBucket);
-
- const x = scaleLinear()
- .domain([xMin, xMax])
- .range([XY_MARGIN.left, XY_WIDTH - XY_MARGIN.right]);
-
- const y = scaleLinear().domain([yMin, yMax]).range([XY_HEIGHT, 0]).nice();
-
- const [xMinZone, xMaxZone] = getDomainTZ(xMin, xMax);
- const xTickValues = isTimeSeries
- ? getTimeTicksTZ({
- domain: [xMinZone, xMaxZone],
- totalTicks: X_TICK_TOTAL,
- width: XY_WIDTH,
- })
- : undefined;
-
- const xDomain = x.domain();
- const yDomain = y.domain();
- const yTickValues = [0, yDomain[1] / 2, yDomain[1]];
- const shouldShowTooltip =
- hoveredBucket.x > 0 && (hoveredBucket.y > 0 || isTimeSeries);
-
- const showVerticalLineHover = verticalLineHover(hoveredBucket);
- const showBackgroundHover = backgroundHover(hoveredBucket);
-
- const hasValidCoordinates = buckets.some((bucket) =>
- isValidCoordinateValue(bucket.y)
- );
- const noHits = this.props.noHits || !hasValidCoordinates;
-
- const xyPlotProps = {
- dontCheckIfEmpty: true,
- xType: this.props.xType,
- width: XY_WIDTH,
- height: XY_HEIGHT,
- margin: XY_MARGIN,
- xDomain: xDomain,
- yDomain: yDomain,
- };
-
- const xAxisProps = {
- style: { strokeWidth: '1px' },
- marginRight: 10,
- tickSize: 0,
- tickTotal: X_TICK_TOTAL,
- tickFormat: formatX,
- tickValues: xTickValues,
- };
-
- const emptyStateChart = (
-
-
-
-
- );
-
- return (
-
-
- {noHits ? (
- <>{emptyStateChart}>
- ) : (
- <>
-
-
-
-
-
- {showBackgroundHover && (
-
- )}
-
- {shouldShowTooltip && (
-
- )}
-
- {selectedBucket && (
-
- )}
-
-
-
- {showVerticalLineHover && hoveredBucket?.x && (
-
- )}
-
- {
- return {
- ...bucket,
- xCenter: (bucket.x0 + bucket.x) / 2,
- };
- })}
- onClick={this.onClick}
- onHover={this.onHover}
- onBlur={this.onBlur}
- x={(d) => x(d.xCenter)}
- y={() => 1}
- />
-
-
- {legends && (
- {}}
- truncateLegends={false}
- noHits={noHits}
- />
- )}
- >
- )}
-
-
- );
- }
-}
-
-HistogramInner.propTypes = {
- backgroundHover: PropTypes.func,
- bucketIndex: PropTypes.number,
- buckets: PropTypes.array.isRequired,
- bucketSize: PropTypes.number.isRequired,
- formatX: PropTypes.func,
- formatYLong: PropTypes.func,
- formatYShort: PropTypes.func,
- onClick: PropTypes.func,
- tooltipFooter: PropTypes.func,
- tooltipHeader: PropTypes.func,
- verticalLineHover: PropTypes.func,
- width: PropTypes.number.isRequired,
- height: PropTypes.number,
- xType: PropTypes.string,
- legends: PropTypes.array,
- noHits: PropTypes.bool,
-};
-
-HistogramInner.defaultProps = {
- backgroundHover: () => null,
- formatYLong: (value) => value,
- formatYShort: (value) => value,
- tooltipFooter: () => null,
- tooltipHeader: () => null,
- verticalLineHover: () => null,
- xType: 'linear',
- noHits: false,
- height: XY_HEIGHT,
-};
-
-export default makeWidthFlexible(HistogramInner);
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
deleted file mode 100644
index 2e4b51af00d6b..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
+++ /dev/null
@@ -1,70 +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 React, { useCallback } from 'react';
-import { Coordinate, TimeSeries } from '../../../../../../typings/timeseries';
-import { useLegacyChartsSync as useChartsSync } from '../../../../../hooks/use_charts_sync';
-// @ts-expect-error
-import CustomPlot from '../../CustomPlot';
-
-interface Props {
- series: TimeSeries[];
- truncateLegends?: boolean;
- tickFormatY: (y: number) => React.ReactNode;
- formatTooltipValue: (c: Coordinate) => React.ReactNode;
- yMax?: string | number;
- height?: number;
- stacked?: boolean;
- onHover?: () => void;
- visibleLegendCount?: number;
- onToggleLegend?: (disabledSeriesState: boolean[]) => void;
-}
-
-function TransactionLineChart(props: Props) {
- const {
- series,
- tickFormatY,
- formatTooltipValue,
- yMax = 'max',
- height,
- truncateLegends,
- stacked = false,
- onHover,
- visibleLegendCount,
- onToggleLegend,
- } = props;
-
- const syncedChartsProps = useChartsSync();
-
- // combine callback for syncedChartsProps.onHover and props.onHover
- const combinedOnHover = useCallback(
- (hoverX: number) => {
- if (onHover) {
- onHover();
- }
- return syncedChartsProps.onHover(hoverX);
- },
- [syncedChartsProps, onHover]
- );
-
- return (
-
- );
-}
-
-export { TransactionLineChart };
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
index b3c0c3b6de857..2a5948d0ebf0b 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
@@ -20,104 +20,107 @@ import {
TRANSACTION_REQUEST,
TRANSACTION_ROUTE_CHANGE,
} from '../../../../../common/transaction_types';
+import { asDecimal, tpmUnit } from '../../../../../common/utils/formatters';
import { Coordinate } from '../../../../../typings/timeseries';
+import { ChartsSyncContextProvider } from '../../../../context/charts_sync_context';
import { LicenseContext } from '../../../../context/LicenseContext';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { ITransactionChartData } from '../../../../selectors/chartSelectors';
-import { asDecimal, tpmUnit } from '../../../../../common/utils/formatters';
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
-import { ErroneousTransactionsRateChart } from '../erroneous_transactions_rate_chart/legacy';
import { TransactionBreakdown } from '../../TransactionBreakdown';
-import {
- getResponseTimeTickFormatter,
- getResponseTimeTooltipFormatter,
-} from './helper';
+import { LineChart } from '../line_chart';
+import { TransactionErrorRateChart } from '../transaction_error_rate_chart/';
+import { getResponseTimeTickFormatter } from './helper';
import { MLHeader } from './ml_header';
-import { TransactionLineChart } from './TransactionLineChart';
import { useFormatter } from './use_formatter';
interface TransactionChartProps {
charts: ITransactionChartData;
urlParams: IUrlParams;
+ fetchStatus: FETCH_STATUS;
}
export function TransactionCharts({
charts,
urlParams,
+ fetchStatus,
}: TransactionChartProps) {
const getTPMFormatter = (t: number) => {
- const unit = tpmUnit(urlParams.transactionType);
- return `${asDecimal(t)} ${unit}`;
+ return `${asDecimal(t)} ${tpmUnit(urlParams.transactionType)}`;
};
- const getTPMTooltipFormatter = (p: Coordinate) => {
- return isValidCoordinateValue(p.y)
- ? getTPMFormatter(p.y)
- : NOT_AVAILABLE_LABEL;
+ const getTPMTooltipFormatter = (y: Coordinate['y']) => {
+ return isValidCoordinateValue(y) ? getTPMFormatter(y) : NOT_AVAILABLE_LABEL;
};
const { transactionType } = urlParams;
const { responseTimeSeries, tpmSeries } = charts;
- const { formatter, setDisabledSeriesState } = useFormatter(
- responseTimeSeries
- );
+ const { formatter, toggleSerie } = useFormatter(responseTimeSeries);
return (
<>
-
-
-
-
-
-
- {responseTimeLabel(transactionType)}
-
-
-
- {(license) => (
-
- )}
-
-
-
-
-
+
+
+
+
+
+
+
+ {responseTimeLabel(transactionType)}
+
+
+
+ {(license) => (
+
+ )}
+
+
+ {
+ if (serie) {
+ toggleSerie(serie);
+ }
+ }}
+ />
+
+
-
-
-
- {tpmLabel(transactionType)}
-
-
-
-
-
+
+
+
+ {tpmLabel(transactionType)}
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx
index fc873cbda7bf2..958a5db6b66c9 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx
@@ -3,38 +3,17 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import { SeriesIdentifier } from '@elastic/charts';
+import { renderHook } from '@testing-library/react-hooks';
+import { act } from 'react-test-renderer';
+import { toMicroseconds } from '../../../../../common/utils/formatters';
import { TimeSeries } from '../../../../../typings/timeseries';
import { useFormatter } from './use_formatter';
-import { render, fireEvent, act } from '@testing-library/react';
-import { toMicroseconds } from '../../../../../common/utils/formatters';
-
-function MockComponent({
- timeSeries,
- disabledSeries,
- value,
-}: {
- timeSeries: TimeSeries[];
- disabledSeries: boolean[];
- value: number;
-}) {
- const { formatter, setDisabledSeriesState } = useFormatter(timeSeries);
-
- const onDisableSeries = () => {
- setDisabledSeriesState(disabledSeries);
- };
-
- return (
-
-
- {formatter(value).formatted}
-
- );
-}
describe('useFormatter', () => {
const timeSeries = ([
{
+ title: 'avg',
data: [
{ x: 1, y: toMicroseconds(11, 'minutes') },
{ x: 2, y: toMicroseconds(1, 'minutes') },
@@ -42,6 +21,7 @@ describe('useFormatter', () => {
],
},
{
+ title: '95th percentile',
data: [
{ x: 1, y: toMicroseconds(120, 'seconds') },
{ x: 2, y: toMicroseconds(1, 'minutes') },
@@ -49,6 +29,7 @@ describe('useFormatter', () => {
],
},
{
+ title: '99th percentile',
data: [
{ x: 1, y: toMicroseconds(60, 'seconds') },
{ x: 2, y: toMicroseconds(5, 'minutes') },
@@ -56,54 +37,47 @@ describe('useFormatter', () => {
],
},
] as unknown) as TimeSeries[];
+
it('returns new formatter when disabled series state changes', () => {
- const { getByText } = render(
-
- );
- expect(getByText('2.0 min')).toBeInTheDocument();
+ const { result } = renderHook(() => useFormatter(timeSeries));
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('2.0 min');
+
act(() => {
- fireEvent.click(getByText('disable series'));
+ result.current.toggleSerie({
+ specId: 'avg',
+ } as SeriesIdentifier);
});
- expect(getByText('120 s')).toBeInTheDocument();
+
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('120 s');
});
+
it('falls back to the first formatter when disabled series is empty', () => {
- const { getByText } = render(
-
- );
- expect(getByText('2.0 min')).toBeInTheDocument();
+ const { result } = renderHook(() => useFormatter(timeSeries));
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('2.0 min');
+
act(() => {
- fireEvent.click(getByText('disable series'));
+ result.current.toggleSerie({
+ specId: 'avg',
+ } as SeriesIdentifier);
});
- expect(getByText('2.0 min')).toBeInTheDocument();
- // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
- // setDisabledSeriesState([true, true, false]);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
- });
- it('falls back to the first formatter when disabled series is all true', () => {
- const { getByText } = render(
-
- );
- expect(getByText('2.0 min')).toBeInTheDocument();
+
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('120 s');
+
act(() => {
- fireEvent.click(getByText('disable series'));
+ result.current.toggleSerie({
+ specId: 'avg',
+ } as SeriesIdentifier);
});
- expect(getByText('2.0 min')).toBeInTheDocument();
- // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
- // setDisabledSeriesState([true, true, false]);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('2.0 min');
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts
index d4694bc3caf1d..1475ec2934e95 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts
@@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useState, Dispatch, SetStateAction } from 'react';
-import { isEmpty } from 'lodash';
+import { SeriesIdentifier } from '@elastic/charts';
+import { omit } from 'lodash';
+import { useState } from 'react';
import {
getDurationFormatter,
TimeFormatter,
@@ -14,17 +15,36 @@ import { TimeSeries } from '../../../../../typings/timeseries';
import { getMaxY } from './helper';
export const useFormatter = (
- series: TimeSeries[]
+ series?: TimeSeries[]
): {
formatter: TimeFormatter;
- setDisabledSeriesState: Dispatch>;
+ toggleSerie: (disabledSerie: SeriesIdentifier) => void;
} => {
- const [disabledSeriesState, setDisabledSeriesState] = useState([]);
- const visibleSeries = series.filter(
- (serie, index) => disabledSeriesState[index] !== true
+ const [disabledSeries, setDisabledSeries] = useState<
+ Record
+ >({});
+
+ const visibleSeries = series?.filter(
+ (serie) => disabledSeries[serie.title] === undefined
);
- const maxY = getMaxY(isEmpty(visibleSeries) ? series : visibleSeries);
+
+ const maxY = getMaxY(visibleSeries || series || []);
const formatter = getDurationFormatter(maxY);
- return { formatter, setDisabledSeriesState };
+ const toggleSerie = ({ specId }: SeriesIdentifier) => {
+ if (disabledSeries[specId] !== undefined) {
+ setDisabledSeries((prevState) => {
+ return omit(prevState, specId);
+ });
+ } else {
+ setDisabledSeries((prevState) => {
+ return { ...prevState, [specId]: 0 };
+ });
+ }
+ };
+
+ return {
+ formatter,
+ toggleSerie,
+ };
};
diff --git a/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx
new file mode 100644
index 0000000000000..683c66b2a96fe
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 {
+ AnnotationDomainTypes,
+ LineAnnotation,
+ Position,
+} from '@elastic/charts';
+import { EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { asAbsoluteDateTime } from '../../../../../common/utils/formatters';
+import { useTheme } from '../../../../hooks/useTheme';
+import { useAnnotations } from '../../../../hooks/use_annotations';
+
+export function Annotations() {
+ const { annotations } = useAnnotations();
+ const theme = useTheme();
+
+ if (!annotations.length) {
+ return null;
+ }
+
+ const color = theme.eui.euiColorSecondary;
+
+ return (
+ ({
+ dataValue: annotation['@timestamp'],
+ header: asAbsoluteDateTime(annotation['@timestamp']),
+ details: `${i18n.translate('xpack.apm.chart.annotation.version', {
+ defaultMessage: 'Version',
+ })} ${annotation.text}`,
+ }))}
+ style={{ line: { strokeWidth: 1, stroke: color, opacity: 1 } }}
+ marker={}
+ markerPosition={Position.Top}
+ />
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
index 409cb69575ca9..c0e8f869ce647 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
@@ -5,30 +5,97 @@
*/
import { render } from '@testing-library/react';
import React from 'react';
+import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { ChartContainer } from './chart_container';
describe('ChartContainer', () => {
- describe('when isLoading is true', () => {
- it('shows loading the indicator', () => {
- const component = render(
-
+ describe('loading indicator', () => {
+ it('shows loading when status equals to Loading or Pending and has no data', () => {
+ [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => {
+ const { queryAllByTestId } = render(
+
+ My amazing component
+
+ );
+
+ expect(queryAllByTestId('loading')[0]).toBeInTheDocument();
+ });
+ });
+ it('does not show loading when status equals to Loading or Pending and has data', () => {
+ [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => {
+ const { queryAllByText } = render(
+
+ My amazing component
+
+ );
+ expect(queryAllByText('My amazing component')[0]).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('failure indicator', () => {
+ it('shows failure message when status equals to Failure and has data', () => {
+ const { getByText } = render(
+
My amazing component
);
-
- expect(component.getByTestId('loading')).toBeInTheDocument();
+ expect(
+ getByText(
+ 'An error happened when trying to fetch data. Please try again'
+ )
+ ).toBeInTheDocument();
+ });
+ it('shows failure message when status equals to Failure and has no data', () => {
+ const { getByText } = render(
+
+ My amazing component
+
+ );
+ expect(
+ getByText(
+ 'An error happened when trying to fetch data. Please try again'
+ )
+ ).toBeInTheDocument();
});
});
- describe('when isLoading is false', () => {
- it('does not show the loading indicator', () => {
- const component = render(
-
+ describe('render component', () => {
+ it('shows children component when status Success and has data', () => {
+ const { getByText } = render(
+
My amazing component
);
-
- expect(component.queryByTestId('loading')).not.toBeInTheDocument();
+ expect(getByText('My amazing component')).toBeInTheDocument();
+ });
+ it('shows children component when status Success and has no data', () => {
+ const { getByText } = render(
+
+ My amazing component
+
+ );
+ expect(getByText('My amazing component')).toBeInTheDocument();
});
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
index a6f579308597f..b4486f1e9b94a 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
@@ -3,27 +3,56 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiLoadingChart } from '@elastic/eui';
+
+import { EuiLoadingChart, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import React from 'react';
+import { FETCH_STATUS } from '../../../hooks/useFetcher';
interface Props {
- isLoading: boolean;
+ hasData: boolean;
+ status: FETCH_STATUS;
height: number;
children: React.ReactNode;
}
-export function ChartContainer({ isLoading, children, height }: Props) {
+export function ChartContainer({ children, height, status, hasData }: Props) {
+ if (
+ !hasData &&
+ (status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING)
+ ) {
+ return ;
+ }
+
+ if (status === FETCH_STATUS.FAILURE) {
+ return ;
+ }
+
+ return {children}
;
+}
+
+function LoadingChartPlaceholder({ height }: { height: number }) {
return (
- {isLoading && }
- {children}
+
);
}
+
+function FailedChartPlaceholder({ height }: { height: number }) {
+ return (
+
+ {i18n.translate('xpack.apm.chart.error', {
+ defaultMessage:
+ 'An error happened when trying to fetch data. Please try again',
+ })}
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx b/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
deleted file mode 100644
index 29102f606414f..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
+++ /dev/null
@@ -1,112 +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 { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
-import { i18n } from '@kbn/i18n';
-import { max } from 'lodash';
-import React, { useCallback } from 'react';
-import { useParams } from 'react-router-dom';
-import { asPercent } from '../../../../../common/utils/formatters';
-import { useLegacyChartsSync as useChartsSync } from '../../../../hooks/use_charts_sync';
-import { useFetcher } from '../../../../hooks/useFetcher';
-import { useUrlParams } from '../../../../hooks/useUrlParams';
-import { callApmApi } from '../../../../services/rest/createCallApmApi';
-// @ts-expect-error
-import CustomPlot from '../CustomPlot';
-
-const tickFormatY = (y?: number | null) => {
- return asPercent(y || 0, 1);
-};
-
-/**
- * "Legacy" version of this chart using react-vis charts. See index.tsx for the
- * Elastic Charts version.
- *
- * This will be removed with #70290.
- */
-export function ErroneousTransactionsRateChart() {
- const { serviceName } = useParams<{ serviceName?: string }>();
- const { urlParams, uiFilters } = useUrlParams();
- const syncedChartsProps = useChartsSync();
-
- const { start, end, transactionType, transactionName } = urlParams;
-
- const { data } = useFetcher(() => {
- if (serviceName && start && end) {
- return callApmApi({
- pathname:
- '/api/apm/services/{serviceName}/transaction_groups/error_rate',
- params: {
- path: {
- serviceName,
- },
- query: {
- start,
- end,
- transactionType,
- transactionName,
- uiFilters: JSON.stringify(uiFilters),
- },
- },
- });
- }
- }, [serviceName, start, end, uiFilters, transactionType, transactionName]);
-
- const combinedOnHover = useCallback(
- (hoverX: number) => {
- return syncedChartsProps.onHover(hoverX);
- },
- [syncedChartsProps]
- );
-
- const errorRates = data?.transactionErrorRate || [];
- const maxRate = max(errorRates.map((errorRate) => errorRate.y));
-
- return (
-
-
-
- {i18n.translate('xpack.apm.errorRateChart.title', {
- defaultMessage: 'Transaction error rate',
- })}
-
-
-
-
- Number.isFinite(y) ? tickFormatY(y) : 'N/A'
- }
- />
-
- );
-}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
index 3f2a08ecb7641..507acc49d89db 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
@@ -20,15 +20,17 @@ import moment from 'moment';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { TimeSeries } from '../../../../../typings/timeseries';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useChartsSync } from '../../../../hooks/use_charts_sync';
import { unit } from '../../../../style/variables';
+import { Annotations } from '../annotations';
import { ChartContainer } from '../chart_container';
import { onBrushEnd } from '../helper/helper';
interface Props {
id: string;
- isLoading: boolean;
+ fetchStatus: FETCH_STATUS;
onToggleLegend?: LegendItemListener;
timeseries: TimeSeries[];
/**
@@ -38,18 +40,20 @@ interface Props {
/**
* Formatter for legend and tooltip values
*/
- yTickFormat: (y: number) => string;
+ yTickFormat?: (y: number) => string;
+ showAnnotations?: boolean;
}
const XY_HEIGHT = unit * 16;
export function LineChart({
id,
- isLoading,
+ fetchStatus,
onToggleLegend,
timeseries,
yLabelFormat,
yTickFormat,
+ showAnnotations = true,
}: Props) {
const history = useHistory();
const chartRef = React.createRef();
@@ -84,7 +88,7 @@ export function LineChart({
);
return (
-
+
onBrushEnd({ x, history })}
@@ -115,11 +119,13 @@ export function LineChart({
id="y-axis"
ticks={3}
position={Position.Left}
- tickFormat={yTickFormat}
+ tickFormat={yTickFormat ? yTickFormat : yLabelFormat}
labelFormat={yLabelFormat}
showGridLines
/>
+ {showAnnotations && }
+
{timeseries.map((serie) => {
return (
();
const { urlParams, uiFilters } = useUrlParams();
@@ -56,25 +61,32 @@ export function ErroneousTransactionsRateChart() {
const errorRates = data?.transactionErrorRate || [];
return (
-
+
+
+
+ {i18n.translate('xpack.apm.errorRate', {
+ defaultMessage: 'Error rate',
+ })}
+
+
+
+
);
}
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts
index 08d2300c3254a..0705383ecb0ca 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts
@@ -15,7 +15,7 @@ export function useTransactionBreakdown() {
uiFilters,
} = useUrlParams();
- const { data = { timeseries: [] }, error, status } = useFetcher(
+ const { data = { timeseries: undefined }, error, status } = useFetcher(
(callApmApi) => {
if (serviceName && start && end && transactionType) {
return callApmApi({
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts
index a5096a314388c..8c76225d03486 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts
@@ -10,7 +10,7 @@ import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
import { useUiFilters } from '../context/UrlParamsContext';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution';
+import type { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution';
import { toQuery, fromQuery } from '../components/shared/Links/url_helpers';
import { maybe } from '../../common/utils/maybe';
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
index 9c3a18b9c0d0d..b2c2cc30f78ec 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
@@ -14,8 +14,8 @@ type TransactionsAPIResponse = APIReturnType<
'/api/apm/services/{serviceName}/transaction_groups'
>;
-const DEFAULT_RESPONSE: TransactionsAPIResponse = {
- items: [],
+const DEFAULT_RESPONSE: Partial = {
+ items: undefined,
isAggregationAccurate: true,
bucketSize: 0,
};
diff --git a/x-pack/plugins/apm/public/hooks/use_annotations.ts b/x-pack/plugins/apm/public/hooks/use_annotations.ts
new file mode 100644
index 0000000000000..2b1c2bec52b3d
--- /dev/null
+++ b/x-pack/plugins/apm/public/hooks/use_annotations.ts
@@ -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 { useParams } from 'react-router-dom';
+import { callApmApi } from '../services/rest/createCallApmApi';
+import { useFetcher } from './useFetcher';
+import { useUrlParams } from './useUrlParams';
+
+const INITIAL_STATE = { annotations: [] };
+
+export function useAnnotations() {
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { urlParams, uiFilters } = useUrlParams();
+ const { start, end } = urlParams;
+ const { environment } = uiFilters;
+
+ const { data = INITIAL_STATE } = useFetcher(() => {
+ if (start && end && serviceName) {
+ return callApmApi({
+ pathname: '/api/apm/services/{serviceName}/annotation/search',
+ params: {
+ path: {
+ serviceName,
+ },
+ query: {
+ start,
+ end,
+ environment,
+ },
+ },
+ });
+ }
+ }, [start, end, environment, serviceName]);
+
+ return data;
+}
diff --git a/x-pack/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
index 8c6093859f969..450f02f70c6a4 100644
--- a/x-pack/plugins/apm/public/selectors/chartSelectors.ts
+++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
@@ -31,40 +31,37 @@ export interface ITpmBucket {
}
export interface ITransactionChartData {
- tpmSeries: ITpmBucket[];
- responseTimeSeries: TimeSeries[];
+ tpmSeries?: ITpmBucket[];
+ responseTimeSeries?: TimeSeries[];
mlJobId: string | undefined;
}
-const INITIAL_DATA = {
- apmTimeseries: {
- responseTimes: {
- avg: [],
- p95: [],
- p99: [],
- },
- tpmBuckets: [],
- overallAvgDuration: null,
- },
+const INITIAL_DATA: Partial = {
+ apmTimeseries: undefined,
anomalyTimeseries: undefined,
};
export function getTransactionCharts(
{ transactionType }: IUrlParams,
- { apmTimeseries, anomalyTimeseries }: TimeSeriesAPIResponse = INITIAL_DATA
+ charts = INITIAL_DATA
): ITransactionChartData {
- const tpmSeries = getTpmSeries(apmTimeseries, transactionType);
-
- const responseTimeSeries = getResponseTimeSeries({
- apmTimeseries,
- anomalyTimeseries,
- });
+ const { apmTimeseries, anomalyTimeseries } = charts;
- return {
- tpmSeries,
- responseTimeSeries,
+ const transactionCharts: ITransactionChartData = {
+ tpmSeries: undefined,
+ responseTimeSeries: undefined,
mlJobId: anomalyTimeseries?.jobId,
};
+
+ if (apmTimeseries) {
+ transactionCharts.tpmSeries = getTpmSeries(apmTimeseries, transactionType);
+
+ transactionCharts.responseTimeSeries = getResponseTimeSeries({
+ apmTimeseries,
+ anomalyTimeseries,
+ });
+ }
+ return transactionCharts;
}
export function getResponseTimeSeries({
diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
index a42710947a792..b12dd73a20986 100644
--- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
+++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
@@ -73,6 +73,6 @@ export async function getBuckets({
return {
noHits: resp.hits.total.value === 0,
- buckets,
+ buckets: resp.hits.total.value > 0 ? buckets : [],
};
}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index baa4f37791007..485b24dced346 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -4875,22 +4875,15 @@
"xpack.apm.error.prompt.title": "申し訳ございませんが、エラーが発生しました :(",
"xpack.apm.errorCountAlert.name": "エラー数しきい値",
"xpack.apm.errorCountAlertTrigger.errors": " エラー",
- "xpack.apm.errorGroupDetails.avgLabel": "平均",
"xpack.apm.errorGroupDetails.culpritLabel": "原因",
"xpack.apm.errorGroupDetails.errorGroupTitle": "エラーグループ {errorGroupId}",
"xpack.apm.errorGroupDetails.errorOccurrenceTitle": "エラーのオカレンス",
"xpack.apm.errorGroupDetails.exceptionMessageLabel": "例外メッセージ",
"xpack.apm.errorGroupDetails.logMessageLabel": "ログメッセージ",
- "xpack.apm.errorGroupDetails.noErrorsLabel": "エラーが見つかりませんでした",
"xpack.apm.errorGroupDetails.occurrencesChartLabel": "オカレンス",
- "xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} {occCount, plural, one {件の発生} other {件の発生}}",
- "xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 件",
"xpack.apm.errorGroupDetails.relatedTransactionSample": "関連トランザクションサンプル",
"xpack.apm.errorGroupDetails.unhandledLabel": "未対応",
"xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "ディスカバリで {occurrencesCount} 件の{occurrencesCount, plural, one {ドキュメント} other {ドキュメント}}を表示。",
- "xpack.apm.errorRateChart.avgLabel": "平均",
- "xpack.apm.errorRateChart.rateLabel": "レート",
- "xpack.apm.errorRateChart.title": "トランザクションエラー率",
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "エラーメッセージと原因",
"xpack.apm.errorsTable.groupIdColumnDescription": "スタックトレースのハッシュ。動的パラメータのため、エラーメッセージが異なる場合でも、類似したエラーをグループ化します。",
"xpack.apm.errorsTable.groupIdColumnLabel": "グループ ID",
@@ -4917,7 +4910,6 @@
"xpack.apm.header.badge.readOnly.text": "読み込み専用",
"xpack.apm.header.badge.readOnly.tooltip": "を保存できませんでした",
"xpack.apm.helpMenu.upgradeAssistantLink": "アップグレードアシスタント",
- "xpack.apm.histogram.plot.noDataLabel": "この時間範囲のデータがありません。",
"xpack.apm.home.alertsMenu.alerts": "アラート",
"xpack.apm.home.alertsMenu.createAnomalyAlert": "異常アラートを作成",
"xpack.apm.home.alertsMenu.createThresholdAlert": "しきい値アラートを作成",
@@ -5256,7 +5248,6 @@
"xpack.apm.transactionDetails.traceNotFound": "選択されたトレースが見つかりません",
"xpack.apm.transactionDetails.traceSampleTitle": "トレースのサンプル",
"xpack.apm.transactionDetails.transactionLabel": "トランザクション",
- "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "このバケットに利用可能なサンプルがありません",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# request} 1 {# 件のリクエスト} other {# 件のリクエスト}}",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel": "{transCount, plural, =0 {# transaction} 1 {# 件のトランザクション} other {# 件のトランザクション}}",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel": "{transCount} {transType, select, request {件のリクエスト} other {件のトランザクション}}",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index c4274524928fd..98d13011d3306 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -4877,22 +4877,15 @@
"xpack.apm.error.prompt.title": "抱歉,发生错误 :(",
"xpack.apm.errorCountAlert.name": "错误计数阈值",
"xpack.apm.errorCountAlertTrigger.errors": " 错误",
- "xpack.apm.errorGroupDetails.avgLabel": "平均",
"xpack.apm.errorGroupDetails.culpritLabel": "原因",
"xpack.apm.errorGroupDetails.errorGroupTitle": "错误组 {errorGroupId}",
"xpack.apm.errorGroupDetails.errorOccurrenceTitle": "错误发生",
"xpack.apm.errorGroupDetails.exceptionMessageLabel": "异常消息",
"xpack.apm.errorGroupDetails.logMessageLabel": "日志消息",
- "xpack.apm.errorGroupDetails.noErrorsLabel": "未找到任何错误",
"xpack.apm.errorGroupDetails.occurrencesChartLabel": "发生次数",
- "xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} 次{occCount, plural, one {出现} other {出现}}",
- "xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 次发生",
"xpack.apm.errorGroupDetails.relatedTransactionSample": "相关的事务样本",
"xpack.apm.errorGroupDetails.unhandledLabel": "未处理",
"xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "在 Discover 查看 {occurrencesCount} 个 {occurrencesCount, plural, one {匹配项} other {匹配项}}。",
- "xpack.apm.errorRateChart.avgLabel": "平均",
- "xpack.apm.errorRateChart.rateLabel": "比率",
- "xpack.apm.errorRateChart.title": "事务错误率",
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "错误消息和原因",
"xpack.apm.errorsTable.groupIdColumnDescription": "堆栈跟踪的哈希。将类似错误分组在一起,即使因动态参数造成错误消息不同。",
"xpack.apm.errorsTable.groupIdColumnLabel": "组 ID",
@@ -4919,7 +4912,6 @@
"xpack.apm.header.badge.readOnly.text": "只读",
"xpack.apm.header.badge.readOnly.tooltip": "无法保存",
"xpack.apm.helpMenu.upgradeAssistantLink": "升级助手",
- "xpack.apm.histogram.plot.noDataLabel": "此时间范围内没有数据。",
"xpack.apm.home.alertsMenu.alerts": "告警",
"xpack.apm.home.alertsMenu.createAnomalyAlert": "创建异常告警",
"xpack.apm.home.alertsMenu.createThresholdAlert": "创建阈值告警",
@@ -5260,7 +5252,6 @@
"xpack.apm.transactionDetails.traceNotFound": "找不到所选跟踪",
"xpack.apm.transactionDetails.traceSampleTitle": "跟踪样例",
"xpack.apm.transactionDetails.transactionLabel": "事务",
- "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "此存储桶没有可用样例",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# 个请求} one {# 个请求} other {# 个请求}}",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel": "{transCount, plural, =0 {# 个事务} one {# 个事务} other {# 个事务}}",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel": "{transCount} 个{transType, select, request {请求} other {事务}}",