From a80366b36481c8efb3046a7766e90fe241f1b5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 3 Dec 2019 09:49:46 +0100 Subject: [PATCH] [APM] Pagination of top 10 trace samples (#51911) * adding trace pagination * adding trace pagination * refactoring * refactoring --- .../__jest__/distribution.test.js | 54 ------- .../__test__/distribution.test.ts | 63 +++++++++ .../TransactionDetails/Distribution/index.tsx | 39 +++--- .../MaybeViewTraceLink.tsx | 87 ++++++++++++ .../WaterfallWithSummmary/index.tsx | 132 +++++++----------- .../app/TransactionDetails/index.tsx | 29 +++- .../Histogram/__test__/Histogram.test.js | 16 ++- .../charts/Histogram/__test__/response.json | 32 +++-- .../hooks/useTransactionDistribution.ts | 15 +- .../distribution/get_buckets/fetcher.ts | 18 ++- .../distribution/get_buckets/transform.ts | 18 ++- 11 files changed, 296 insertions(+), 207 deletions(-) delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__jest__/distribution.test.js create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__jest__/distribution.test.js b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__jest__/distribution.test.js deleted file mode 100644 index 75338c669d0b3..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__jest__/distribution.test.js +++ /dev/null @@ -1,54 +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 { getFormattedBuckets } from '../index'; - -describe('Distribution', () => { - it('getFormattedBuckets', () => { - const buckets = [ - { key: 0, count: 0 }, - { key: 20, count: 0 }, - { key: 40, count: 0 }, - { - key: 60, - count: 5, - sample: { - transactionId: 'someTransactionId' - } - }, - { - key: 80, - count: 100, - sample: { - transactionId: 'anotherTransactionId' - } - } - ]; - expect(getFormattedBuckets(buckets, 20)).toEqual([ - { x: 20, x0: 0, y: 0, style: { cursor: 'default' } }, - { x: 40, x0: 20, y: 0, style: { cursor: 'default' } }, - { x: 60, x0: 40, y: 0, style: { cursor: 'default' } }, - { - x: 80, - x0: 60, - y: 5, - sample: { - transactionId: 'someTransactionId' - }, - style: { cursor: 'pointer' } - }, - { - x: 100, - x0: 80, - y: 100, - sample: { - transactionId: 'anotherTransactionId' - }, - style: { cursor: 'pointer' } - } - ]); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts new file mode 100644 index 0000000000000..85398a1324f7b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { getFormattedBuckets } from '../index'; +import { IBucket } from '../../../../../../server/lib/transactions/distribution/get_buckets/transform'; + +describe('Distribution', () => { + it('getFormattedBuckets', () => { + const buckets = [ + { key: 0, count: 0, samples: [] }, + { key: 20, count: 0, samples: [] }, + { key: 40, count: 0, samples: [] }, + { + key: 60, + count: 5, + samples: [ + { + transactionId: 'someTransactionId' + } + ] + }, + { + key: 80, + count: 100, + samples: [ + { + transactionId: 'anotherTransactionId' + } + ] + } + ] as IBucket[]; + expect(getFormattedBuckets(buckets, 20)).toEqual([ + { x: 20, x0: 0, y: 0, style: { cursor: 'default' }, samples: [] }, + { x: 40, x0: 20, y: 0, style: { cursor: 'default' }, samples: [] }, + { x: 60, x0: 40, y: 0, style: { cursor: 'default' }, samples: [] }, + { + x: 80, + x0: 60, + y: 5, + style: { cursor: 'pointer' }, + samples: [ + { + transactionId: 'someTransactionId' + } + ] + }, + { + x: 100, + x0: 80, + y: 100, + style: { cursor: 'pointer' }, + samples: [ + { + transactionId: 'anotherTransactionId' + } + ] + } + ]); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index c9e5175a10921..acd50438b4d54 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -8,6 +8,7 @@ import { EuiIconTip, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import d3 from 'd3'; import React, { FunctionComponent, useCallback } from 'react'; +import { isEmpty } from 'lodash'; import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; @@ -20,7 +21,7 @@ import { history } from '../../../../utils/history'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; interface IChartPoint { - sample?: IBucket['sample']; + samples: IBucket['samples']; x0: number; x: number; y: number; @@ -35,13 +36,15 @@ export function getFormattedBuckets(buckets: IBucket[], bucketSize: number) { } return buckets.map( - ({ sample, count, key }): IChartPoint => { + ({ samples, count, key }): IChartPoint => { return { - sample, + samples, x0: key, x: key + bucketSize, y: count, - style: { cursor: count > 0 && sample ? 'pointer' : 'default' } + style: { + cursor: isEmpty(samples) ? 'default' : 'pointer' + } }; } ); @@ -91,6 +94,7 @@ interface Props { distribution?: TransactionDistributionAPIResponse; urlParams: IUrlParams; isLoading: boolean; + bucketIndex: number; } export const TransactionDistribution: FunctionComponent = ( @@ -98,8 +102,9 @@ export const TransactionDistribution: FunctionComponent = ( ) => { const { distribution, - urlParams: { transactionId, traceId, transactionType }, - isLoading + urlParams: { transactionType }, + isLoading, + bucketIndex } = props; const formatYShort = useCallback(getFormatYShort(transactionType), [ @@ -134,13 +139,6 @@ export const TransactionDistribution: FunctionComponent = ( const xMax = d3.max(buckets, d => d.x) || 0; const timeFormatter = getDurationFormatter(xMax); - const bucketIndex = buckets.findIndex( - bucket => - bucket.sample != null && - bucket.sample.transactionId === transactionId && - bucket.sample.traceId === traceId - ); - return (
@@ -175,13 +173,14 @@ export const TransactionDistribution: FunctionComponent = ( bucketSize={distribution.bucketSize} bucketIndex={bucketIndex} onClick={(bucket: IChartPoint) => { - if (bucket.sample && bucket.y > 0) { + if (!isEmpty(bucket.samples)) { + const sample = bucket.samples[0]; history.push({ ...history.location, search: fromQuery({ ...toQuery(history.location.search), - transactionId: bucket.sample.transactionId, - traceId: bucket.sample.traceId + transactionId: sample.transactionId, + traceId: sample.traceId }) }); } @@ -189,17 +188,15 @@ export const TransactionDistribution: FunctionComponent = ( 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} + verticalLineHover={(bucket: IChartPoint) => isEmpty(bucket.samples)} + backgroundHover={(bucket: IChartPoint) => !isEmpty(bucket.samples)} 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 && + isEmpty(bucket.samples) && i18n.translate( 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip', { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx new file mode 100644 index 0000000000000..39e52be34a415 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx @@ -0,0 +1,87 @@ +/* + * 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 { EuiButton, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/Transaction'; +import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; + +export const MaybeViewTraceLink = ({ + transaction, + waterfall +}: { + transaction: ITransaction; + waterfall: IWaterfall; +}) => { + const viewFullTraceButtonLabel = i18n.translate( + 'xpack.apm.transactionDetails.viewFullTraceButtonLabel', + { + defaultMessage: 'View full trace' + } + ); + + // the traceroot cannot be found, so we cannot link to it + if (!waterfall.traceRoot) { + return ( + + + + {viewFullTraceButtonLabel} + + + + ); + } + + const isRoot = + transaction.transaction.id === waterfall.traceRoot.transaction.id; + + // the user is already viewing the full trace, so don't link to it + if (isRoot) { + return ( + + + + {viewFullTraceButtonLabel} + + + + ); + + // the user is viewing a zoomed in version of the trace. Link to the full trace + } else { + const traceRoot = waterfall.traceRoot; + return ( + + + {viewFullTraceButtonLabel} + + + ); + } +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index fb37bf44c2f32..b56370a59c8e2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -5,102 +5,44 @@ */ import { - EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, - EuiToolTip, EuiEmptyPrompt, - EuiTitle + EuiTitle, + EuiPagination } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { sum } from 'lodash'; -import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/Transaction'; +import styled from 'styled-components'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; import { TransactionTabs } from './TransactionTabs'; import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; +import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; +import { history } from '../../../../utils/history'; +import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { MaybeViewTraceLink } from './MaybeViewTraceLink'; +import { units, px } from '../../../../style/variables'; -function MaybeViewTraceLink({ - transaction, - waterfall -}: { - transaction: ITransaction; - waterfall: IWaterfall; -}) { - const viewFullTraceButtonLabel = i18n.translate( - 'xpack.apm.transactionDetails.viewFullTraceButtonLabel', - { - defaultMessage: 'View full trace' - } - ); +const PaginationContainer = styled.div` + margin-left: ${px(units.quarter)}; + display: flex; + align-items: center; - // the traceroot cannot be found, so we cannot link to it - if (!waterfall.traceRoot) { - return ( - - - - {viewFullTraceButtonLabel} - - - - ); + > span:first-of-type { + font-weight: 600; } - const isRoot = - transaction.transaction.id === waterfall.traceRoot.transaction.id; - - // the user is already viewing the full trace, so don't link to it - if (isRoot) { - return ( - - - - {viewFullTraceButtonLabel} - - - - ); - - // the user is viewing a zoomed in version of the trace. Link to the full trace - } else { - const traceRoot = waterfall.traceRoot; - return ( - - - {viewFullTraceButtonLabel} - - - ); + > span:last-of-type { + margin-right: ${px(units.half)}; } -} +`; interface Props { urlParams: IUrlParams; @@ -108,6 +50,7 @@ interface Props { waterfall: IWaterfall; exceedsMax: boolean; isLoading: boolean; + traceSamples: IBucket['samples']; } export const WaterfallWithSummmary: React.FC = ({ @@ -115,8 +58,28 @@ export const WaterfallWithSummmary: React.FC = ({ location, waterfall, exceedsMax, - isLoading + isLoading, + traceSamples }) => { + const [sampleActivePage, setSampleActivePage] = useState(0); + + useEffect(() => { + setSampleActivePage(0); + }, [traceSamples]); + + const goToSample = (index: number) => { + setSampleActivePage(index); + const sample = traceSamples[index]; + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + transactionId: sample.transactionId, + traceId: sample.traceId + }) + }); + }; + const { entryTransaction } = waterfall; if (!entryTransaction) { const content = isLoading ? ( @@ -140,7 +103,7 @@ export const WaterfallWithSummmary: React.FC = ({ return ( - +
{i18n.translate('xpack.apm.transactionDetails.traceSampleTitle', { @@ -148,8 +111,19 @@ export const WaterfallWithSummmary: React.FC = ({ })}
+ {traceSamples && ( + + {sampleActivePage + 1} + /{traceSamples.length} + + + )}
- diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx index e4b2608343fac..dbd0e9d3b9d22 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -29,6 +29,7 @@ import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../infra/public'; import { PROJECTION } from '../../../../common/projections/typings'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { HeightRetainer } from '../../shared/HeightRetainer'; export function TransactionDetails() { const location = useLocation(); @@ -60,6 +61,16 @@ export function TransactionDetails() { return config; }, [transactionName, transactionType, serviceName]); + const bucketIndex = distributionData.buckets.findIndex(bucket => + bucket.samples.some( + sample => + sample.transactionId === urlParams.transactionId && + sample.traceId === urlParams.traceId + ) + ); + + const traceSamples = distributionData.buckets[bucketIndex]?.samples; + return (
@@ -93,18 +104,22 @@ export function TransactionDetails() { distribution={distributionData} isLoading={distributionStatus === FETCH_STATUS.LOADING} urlParams={urlParams} + bucketIndex={bucketIndex} /> - + + +
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js index f76a27480137a..ec704d7405019 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js @@ -95,9 +95,11 @@ describe('Histogram', () => { it('should update state with "hoveredBucket"', () => { expect(wrapper.state()).toEqual({ hoveredBucket: { - sample: { - transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192' - }, + samples: [ + { + transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192' + } + ], style: { cursor: 'pointer' }, xCenter: 869010, x0: 811076, @@ -123,9 +125,11 @@ describe('Histogram', () => { it('should call onClick with bucket', () => { expect(onClick).toHaveBeenCalledWith({ - sample: { - transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192' - }, + samples: [ + { + transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192' + } + ], style: { cursor: 'pointer' }, xCenter: 869010, x0: 811076, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json index f48213f72a983..302e105dfa997 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json @@ -8,23 +8,29 @@ { "key": 579340, "count": 8, - "sample": { - "transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb" - } + "samples": [ + { + "transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb" + } + ] }, { "key": 695208, "count": 23, - "sample": { - "transactionId": "d327611b-e999-4942-a94f-c60208940180" - } + "samples": [ + { + "transactionId": "d327611b-e999-4942-a94f-c60208940180" + } + ] }, { "key": 811076, "count": 49, - "sample": { - "transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192" - } + "samples": [ + { + "transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192" + } + ] }, { "key": 926944, @@ -39,9 +45,11 @@ { "key": 1158680, "count": 13, - "sample": { - "transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2" - } + "samples": [ + { + "transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2" + } + ] }, { "key": 1274548, diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts index 4c7e337212e55..e50ea7eab187f 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -49,19 +49,10 @@ export function useTransactionDistribution(urlParams: IUrlParams) { } }); } - // the histogram should not be refetched if the transactionId or traceId changes - // eslint-disable-next-line react-hooks/exhaustive-deps }, - [ - serviceName, - start, - end, - transactionType, - transactionName, - transactionId, - traceId, - uiFilters - ] + // the histogram should not be refetched if the transactionId or traceId changes + // eslint-disable-next-line react-hooks/exhaustive-deps + [serviceName, start, end, transactionType, transactionName, uiFilters] ); return { data, status, error }; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index 32fa65722869d..b2f0834107df4 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -50,8 +50,7 @@ export async function bucketFetcher( ], should: [ { term: { [TRACE_ID]: traceId } }, - { term: { [TRANSACTION_ID]: transactionId } }, - { term: { [TRANSACTION_SAMPLED]: true } } + { term: { [TRANSACTION_ID]: transactionId } } ] } }, @@ -67,10 +66,17 @@ export async function bucketFetcher( } }, aggs: { - sample: { - top_hits: { - _source: [TRANSACTION_ID, TRANSACTION_SAMPLED, TRACE_ID], - size: 1 + samples: { + filter: { + term: { [TRANSACTION_SAMPLED]: true } + }, + aggs: { + items: { + top_hits: { + _source: [TRANSACTION_ID, TRACE_ID], + size: 10 + } + } } } } diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts index a16e08138b87f..2e703dfb19680 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts @@ -11,25 +11,23 @@ import { bucketFetcher } from './fetcher'; type DistributionBucketResponse = PromiseReturnType; export type IBucket = ReturnType; + function getBucket( bucket: Required< DistributionBucketResponse >['aggregations']['distribution']['buckets'][0] ) { - const sampleSource = bucket.sample.hits.hits[0]?._source as - | Transaction - | undefined; - - const isSampled = sampleSource?.transaction.sampled; - const sample = { - traceId: sampleSource?.trace.id, - transactionId: sampleSource?.transaction.id - }; + const samples = bucket.samples.items.hits.hits.map( + ({ _source }: { _source: Transaction }) => ({ + traceId: _source.trace.id, + transactionId: _source.transaction.id + }) + ); return { key: bucket.key, count: bucket.doc_count, - sample: isSampled ? sample : undefined + samples }; }