[Discover][APM] Add duration section to overview tab in flyout and include basic duration information (#216291)

## Summary

Closes https://github.com/elastic/kibana/issues/214446

This PR introduces a new section in the span/transaction overview flyout
that will display the duration information along with a latency chart.
For now, only the duration data is included.

|Scenario||
|-|-|
|Span w/ `transaction.id`|![Screenshot 2025-03-28 at 13 00
04](https://github.com/user-attachments/assets/66b54f58-0474-4424-81ad-688ae0492273)|
|Span w/o `transaction.id`|![Screenshot 2025-03-28 at 13 00
27](https://github.com/user-attachments/assets/ed76c0e4-e5a3-465a-86b4-4e507237f5ac)|
|Transaction w/ `parent.id`|![Screenshot 2025-03-28 at 13 01
25](https://github.com/user-attachments/assets/14ec2d14-33ab-41de-a2e7-3c3d85f69cc3)|
|Transaction w/o `parent.id` (root transaction)|![Screenshot 2025-03-28
at 13 01
57](https://github.com/user-attachments/assets/5345bee5-3b64-45b8-91e3-374444b11b40)|


Given that both spans and transactions require retrieving data from
their respective parents, a loader has been added to reflect the ongoing
data-fetching process:

![Screen Recording 2025-03-28 at 13 02
04](https://github.com/user-attachments/assets/6fdebfda-c5e2-487a-a3af-e84d192fd512)

## Tech details

The UI for displaying the duration and its percentage relative to the
parent is already in use in APM for spans and transactions, so the logic
is consistent with that.

To avoid duplicating components and formatters, as seen in previous PRs
for Traces in Discover, a new `Duration` component has been created in a
newly created `kbn-apm-ui-shared` package. This component will be used
in Discover and [later](https://github.com/elastic/kibana/issues/211781)
in APM as well.

## How to test

- Enable the discover profiles by adding this to the` kibana.yml `file:
```discover.experimental.enabledProfiles:
  - observability-traces-data-source-profile
  - observability-traces-transaction-document-profile
  - observability-traces-span-document-profile
```
- Open Discover and select or create a data view that includes any APM
traces index (`traces-*`), or query them using ES|QL.
- Apply a filter for `data_stream.type:"traces"` to ensure only trace
documents are retrieved.
- Open the flyout.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>
This commit is contained in:
Irene Blanco 2025-04-03 11:37:55 +02:00 committed by GitHub
parent 968dd5554e
commit 5c950b4f3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 876 additions and 133 deletions

1
.github/CODEOWNERS vendored
View file

@ -411,6 +411,7 @@ src/platform/packages/shared/kbn-analytics @elastic/kibana-core
src/platform/packages/shared/kbn-apm-data-view @elastic/obs-ux-infra_services-team
src/platform/packages/shared/kbn-apm-synthtrace @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team
src/platform/packages/shared/kbn-apm-synthtrace-client @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team
src/platform/packages/shared/kbn-apm-ui-shared @elastic/obs-ux-infra_services-team
src/platform/packages/shared/kbn-apm-utils @elastic/obs-ux-infra_services-team
src/platform/packages/shared/kbn-avc-banner @elastic/security-defend-workflows
src/platform/packages/shared/kbn-axe-config @elastic/appex-qa

View file

@ -200,6 +200,7 @@
"@kbn/apm-plugin": "link:x-pack/solutions/observability/plugins/apm",
"@kbn/apm-sources-access-plugin": "link:x-pack/platform/plugins/shared/apm_sources_access",
"@kbn/apm-types": "link:x-pack/platform/packages/shared/kbn-apm-types",
"@kbn/apm-ui-shared": "link:src/platform/packages/shared/kbn-apm-ui-shared",
"@kbn/apm-utils": "link:src/platform/packages/shared/kbn-apm-utils",
"@kbn/app-link-test-plugin": "link:src/platform/test/plugin_functional/plugins/app_link_test",
"@kbn/application-usage-test-plugin": "link:x-pack/test/usage_collection/plugins/application_usage_test",

View file

@ -0,0 +1,19 @@
# @kbn/apm-ui-shared
## Components
| Name | Description |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Duration | Displays the duration with automatic unit formatting and the option to show the percentage of the duration relative to a parent (trace or transaction). |
## Utils
### Formatters
| Name | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| asDuration | Format duration value to the best unit for display. |
| asDecimal | Format a number to a decimal with a fixed number of decimal places. |
| asInteger | Format a number to an integer. |
| asDecimalOrInteger | Format a number to a decimal with a fixed number of decimal places if it has a decimal part, otherwise format it to an integer. |
| asPercent | Format a number to a percentage with a fixed number of decimal places. |

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './src/components/duration';

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../..',
roots: ['<rootDir>/src/platform/packages/shared/kbn-apm-ui-shared'],
setupFilesAfterEnv: ['<rootDir>/src/platform/packages/shared/kbn-apm-ui-shared/setup_tests.ts'],
};

View file

@ -0,0 +1,9 @@
{
"type": "shared-browser",
"id": "@kbn/apm-ui-shared",
"owner": [
"@elastic/obs-ux-infra_services-team"
],
"group": "platform",
"visibility": "shared"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/apm-ui-shared",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import '@testing-library/jest-dom';

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { Duration, DurationProps } from '.';
import { render, screen } from '@testing-library/react';
describe('Duration', () => {
const duration = 10;
const parentDuration = 20;
const expectedDurationText = `${duration} μs`;
const getExpectedParentDurationText = (parentType: string) => `(50% of ${parentType})`;
const loadingDataTestSubj = 'DurationLoadingSpinner';
describe('when there is NOT parent data', () => {
it('should render duration with the right format', () => {
render(<Duration duration={duration} />);
expect(screen.getByText(expectedDurationText)).toBeInTheDocument();
});
});
describe('when there is parent data', () => {
describe('and the loading is set to true', () => {
const parentWithLoading: DurationProps['parent'] = {
duration: parentDuration,
type: 'trace',
loading: true,
};
it('should render the duration and the loader but not the parent duration', () => {
render(<Duration duration={duration} parent={parentWithLoading} />);
expect(screen.getByText(expectedDurationText)).toBeInTheDocument();
expect(screen.getByTestId(loadingDataTestSubj)).toBeInTheDocument();
});
});
describe('and the loading is set to false', () => {
const parentWithLoading: DurationProps['parent'] = {
duration: parentDuration,
type: 'trace',
loading: false,
};
it('should render the duration and the parent duration but not the loader', () => {
render(<Duration duration={duration} parent={parentWithLoading} />);
expect(
screen.getByText(
`${expectedDurationText} ${getExpectedParentDurationText(parentWithLoading.type)}`
)
).toBeInTheDocument();
expect(screen.queryByTestId(loadingDataTestSubj)).not.toBeInTheDocument();
});
});
});
});

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { EuiLoadingSpinner, EuiText } from '@elastic/eui';
import { asDuration } from '../../utils';
import { PercentOfParent } from './percent_of_parent';
export interface DurationProps {
duration: number;
parent?: {
duration?: number;
type: 'trace' | 'transaction';
loading: boolean;
};
}
export function Duration({ duration, parent }: DurationProps) {
if (!parent) {
<EuiText size="xs">{asDuration(duration)}</EuiText>;
}
return (
<EuiText size="xs">
{asDuration(duration)} &nbsp;
{parent?.loading && <EuiLoadingSpinner data-test-subj="DurationLoadingSpinner" />}
{!parent?.loading && parent?.duration && (
<PercentOfParent
duration={duration}
totalDuration={parent?.duration}
parentType={parent?.type}
/>
)}
</EuiText>
);
}

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { asPercent } from '../../utils';
interface PercentOfParentProps {
duration: number;
totalDuration?: number;
parentType: 'trace' | 'transaction';
}
export function PercentOfParent({ duration, totalDuration, parentType }: PercentOfParentProps) {
totalDuration = totalDuration || duration;
const isOver100 = duration > totalDuration;
const percentOfParent = isOver100 ? '>100%' : asPercent(duration, totalDuration, '');
const percentOfParentText = i18n.translate('apmUiShared.duration.percentOfParent', {
defaultMessage:
'({value} of {parentType, select, transaction {transaction} trace {trace} other {unknown parentType}})',
values: { value: percentOfParent, parentType },
});
const childType = parentType === 'trace' ? 'transaction' : 'span';
return (
<>
{isOver100 ? (
<EuiToolTip
content={i18n.translate('apmUiShared.duration.percentOfTraceLabelExplanation', {
defaultMessage:
'The % of {parentType, select, transaction {transaction} trace {trace} other {unknown parentType} } exceeds 100% because this {childType, select, span {span} transaction {transaction} other {unknown childType} } takes longer than the root transaction.',
values: {
parentType,
childType,
},
})}
>
<>{percentOfParentText}</>
</EuiToolTip>
) : (
percentOfParentText
)}
</>
);
}

View file

@ -47,7 +47,7 @@ function getUnitLabelAndConvertedValue(
switch (unitKey) {
case 'hours': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.hoursTimeUnitLabel', {
unitLabel: i18n.translate('apmUiShared.formatters.duration.hoursTimeUnitLabel', {
defaultMessage: 'h',
}),
convertedValue: asDecimalOrInteger(moment.duration(ms).asHours(), threshold),
@ -55,7 +55,7 @@ function getUnitLabelAndConvertedValue(
}
case 'minutes': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.minutesTimeUnitLabel', {
unitLabel: i18n.translate('apmUiShared.formatters.duration.minutesTimeUnitLabel', {
defaultMessage: 'min',
}),
convertedValue: asDecimalOrInteger(moment.duration(ms).asMinutes(), threshold),
@ -63,7 +63,7 @@ function getUnitLabelAndConvertedValue(
}
case 'seconds': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.secondsTimeUnitLabel', {
unitLabel: i18n.translate('apmUiShared.formatters.duration.secondsTimeUnitLabel', {
defaultMessage: 's',
}),
convertedValue: asDecimalOrInteger(moment.duration(ms).asSeconds(), threshold),
@ -71,7 +71,7 @@ function getUnitLabelAndConvertedValue(
}
case 'milliseconds': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.millisTimeUnitLabel', {
unitLabel: i18n.translate('apmUiShared.formatters.duration.millisTimeUnitLabel', {
defaultMessage: 'ms',
}),
convertedValue: asDecimalOrInteger(moment.duration(ms).asMilliseconds(), threshold),
@ -79,7 +79,7 @@ function getUnitLabelAndConvertedValue(
}
case 'microseconds': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.microsTimeUnitLabel', {
unitLabel: i18n.translate('apmUiShared.formatters.duration.microsTimeUnitLabel', {
defaultMessage: 'μs',
}),
convertedValue: asInteger(value),

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { asDecimal, asInteger, asDecimalOrInteger } from './numeric';
import { asDecimal, asInteger, asDecimalOrInteger, asPercent } from './numeric';
describe('formatters', () => {
describe('asDecimal', () => {
@ -107,4 +107,31 @@ describe('formatters', () => {
expect(asDecimalOrInteger(undefined)).toEqual('N/A');
});
});
describe('asPercent', () => {
it('formats as integer when number is above 10', () => {
expect(asPercent(3725, 10000, 'n/a')).toEqual('37%');
});
it('adds a decimal when value is below 10', () => {
expect(asPercent(0.092, 1)).toEqual('9.2%');
});
it('formats when numerator is 0', () => {
expect(asPercent(0, 1, 'n/a')).toEqual('0%');
});
it('returns fallback when denominator is undefined', () => {
expect(asPercent(3725, undefined, 'n/a')).toEqual('n/a');
});
it('returns fallback when denominator is 0 ', () => {
expect(asPercent(3725, 0, 'n/a')).toEqual('n/a');
});
it('returns fallback when numerator or denominator is NaN', () => {
expect(asPercent(3725, NaN, 'n/a')).toEqual('n/a');
expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a');
});
});
});

View file

@ -13,7 +13,7 @@ import numeral from '@elastic/numeral';
import { Maybe } from '../../typings';
export const NOT_AVAILABLE_LABEL = i18n.translate(
'unifiedDocViewer.formatters.numeric.notAvailableLabel',
'apmUiShared.formatters.numeric.notAvailableLabel',
{
defaultMessage: 'N/A',
}
@ -45,3 +45,21 @@ export function asDecimalOrInteger(value: Maybe<number>, threshold = 10) {
}
return asDecimal(value);
}
export function asPercent(
numerator: Maybe<number>,
denominator: number | undefined,
fallbackResult = NOT_AVAILABLE_LABEL
) {
if (numerator === null || numerator === undefined || !denominator || !isFinite(numerator)) {
return fallbackResult;
}
const decimal = numerator / denominator;
if (Math.abs(decimal) >= 0.1 || decimal === 0) {
return numeral(decimal).format('0%');
}
return numeral(decimal).format('0.0%');
}

View file

@ -0,0 +1,12 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": ["jest", "node", "react"]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["target/**/*"],
"kbn_references": [
"@kbn/i18n",
]
}

View file

@ -15,7 +15,6 @@ import { createObservabilityTracesSpanDocumentProfileProvider } from './profile'
import type { ContextWithProfileId } from '../../../../profile_service';
import { OBSERVABILITY_ROOT_PROFILE_ID } from '../../consts';
import type { ProfileProviderServices } from '../../../profile_provider_services';
import { applicationMock } from '../__mocks__/application_mock';
describe('spanDocumentProfileProvider', () => {
const getRootContext = ({
@ -43,12 +42,12 @@ describe('spanDocumentProfileProvider', () => {
isMatch: false,
};
const mockServices: ProfileProviderServices = {
...createContextAwarenessMocks().profileProviderServices,
};
describe('when root profile is observability', () => {
const profileId = OBSERVABILITY_ROOT_PROFILE_ID;
const mockServices: ProfileProviderServices = {
...createContextAwarenessMocks().profileProviderServices,
...applicationMock({ apm: { show: true } }),
};
const spanDocumentProfileProvider =
createObservabilityTracesSpanDocumentProfileProvider(mockServices);
@ -79,10 +78,6 @@ describe('spanDocumentProfileProvider', () => {
describe('when root profile is NOT observability', () => {
const profileId = 'another-profile';
const mockServices: ProfileProviderServices = {
...createContextAwarenessMocks().profileProviderServices,
...applicationMock({}),
};
const spanDocumentProfileProvider =
createObservabilityTracesSpanDocumentProfileProvider(mockServices);

View file

@ -11,9 +11,11 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { UnifiedDocViewerObservabilityTracesTransactionOverview } from '@kbn/unified-doc-viewer-plugin/public';
import type { DocViewsRegistry } from '@kbn/unified-doc-viewer';
import type { DocumentProfileProvider } from '../../../../..';
import type { DocViewerExtensionParams, DocViewerExtension } from '../../../../../types';
export const getDocViewer =
export const createGetDocViewer =
(tracesIndexPattern: string): DocumentProfileProvider['profile']['getDocViewer'] =>
(prev: (params: DocViewerExtensionParams) => DocViewerExtension) =>
(params: DocViewerExtensionParams) => {
const prevDocViewer = prev(params);
@ -31,7 +33,12 @@ export const getDocViewer =
),
order: 0,
component: (props) => {
return <UnifiedDocViewerObservabilityTracesTransactionOverview {...props} />;
return (
<UnifiedDocViewerObservabilityTracesTransactionOverview
{...props}
tracesIndexPattern={tracesIndexPattern}
/>
);
},
});

View file

@ -7,4 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { getDocViewer } from './doc_viewer';
export { createGetDocViewer } from './doc_viewer';

View file

@ -13,6 +13,8 @@ import { DataSourceCategory, DocumentType, SolutionType } from '../../../../prof
import { createObservabilityTracesTransactionDocumentProfileProvider } from './profile';
import type { ContextWithProfileId } from '../../../../profile_service';
import { OBSERVABILITY_ROOT_PROFILE_ID } from '../../consts';
import { createContextAwarenessMocks } from '../../../../__mocks__';
import type { ProfileProviderServices } from '../../../profile_provider_services';
describe('transactionDocumentProfileProvider', () => {
const getRootContext = ({
@ -40,10 +42,14 @@ describe('transactionDocumentProfileProvider', () => {
isMatch: false,
};
const mockServices: ProfileProviderServices = {
...createContextAwarenessMocks().profileProviderServices,
};
describe('when root profile is observability', () => {
const profileId = OBSERVABILITY_ROOT_PROFILE_ID;
const transactionDocumentProfileProvider =
createObservabilityTracesTransactionDocumentProfileProvider();
createObservabilityTracesTransactionDocumentProfileProvider(mockServices);
it('matches records with the correct data stream type and the correct processor event', () => {
expect(
@ -72,7 +78,7 @@ describe('transactionDocumentProfileProvider', () => {
describe('when root profile is NOT observability', () => {
const profileId = 'another-profile';
const transactionDocumentProfileProvider =
createObservabilityTracesTransactionDocumentProfileProvider();
createObservabilityTracesTransactionDocumentProfileProvider(mockServices);
it('does not match records with the correct data stream type and the correct processor event', () => {
expect(

View file

@ -11,42 +11,44 @@ import type { DataTableRecord } from '@kbn/discover-utils';
import { DATASTREAM_TYPE_FIELD, getFieldValue, PROCESSOR_EVENT_FIELD } from '@kbn/discover-utils';
import type { DocumentProfileProvider } from '../../../../profiles';
import { DocumentType } from '../../../../profiles';
import { getDocViewer } from './accessors';
import { createGetDocViewer } from './accessors';
import { OBSERVABILITY_ROOT_PROFILE_ID } from '../../consts';
import type { ProfileProviderServices } from '../../../profile_provider_services';
const OBSERVABILITY_TRACES_TRANSACTION_DOCUMENT_PROFILE_ID =
'observability-traces-transaction-document-profile';
export const createObservabilityTracesTransactionDocumentProfileProvider =
(): DocumentProfileProvider => ({
isExperimental: true,
profileId: OBSERVABILITY_TRACES_TRANSACTION_DOCUMENT_PROFILE_ID,
profile: {
getDocViewer,
},
resolve: ({ record, rootContext }) => {
const isObservabilitySolutionView = rootContext.profileId === OBSERVABILITY_ROOT_PROFILE_ID;
export const createObservabilityTracesTransactionDocumentProfileProvider = ({
tracesContextService,
}: ProfileProviderServices): DocumentProfileProvider => ({
isExperimental: true,
profileId: OBSERVABILITY_TRACES_TRANSACTION_DOCUMENT_PROFILE_ID,
profile: {
getDocViewer: createGetDocViewer(tracesContextService.getAllTracesIndexPattern() || ''),
},
resolve: ({ record, rootContext }) => {
const isObservabilitySolutionView = rootContext.profileId === OBSERVABILITY_ROOT_PROFILE_ID;
if (!isObservabilitySolutionView) {
return { isMatch: false };
}
if (!isObservabilitySolutionView) {
return { isMatch: false };
}
const isTransactionRecord = getIsTransactionRecord({
record,
});
const isTransactionRecord = getIsTransactionRecord({
record,
});
if (!isTransactionRecord) {
return { isMatch: false };
}
if (!isTransactionRecord) {
return { isMatch: false };
}
return {
isMatch: true,
context: {
type: DocumentType.Transaction,
},
};
},
});
return {
isMatch: true,
context: {
type: DocumentType.Transaction,
},
};
},
});
const getIsTransactionRecord = ({ record }: { record: DataTableRecord }) => {
return isTransactionDocument(record);

View file

@ -151,5 +151,5 @@ const createDocumentProfileProviders = (providerServices: ProfileProviderService
createExampleDocumentProfileProvider(),
createObservabilityLogDocumentProfileProvider(providerServices),
createObservabilityTracesSpanDocumentProfileProvider(providerServices),
createObservabilityTracesTransactionDocumentProfileProvider(),
createObservabilityTracesTransactionDocumentProfileProvider(providerServices),
];

View file

@ -12,7 +12,7 @@ import { renderHook, waitFor } from '@testing-library/react';
import { lastValueFrom } from 'rxjs';
import { getUnifiedDocViewerServices } from '../../../../../plugin';
import { TransactionProvider, useTransactionContext } from './use_transaction';
import { TRANSACTION_NAME_FIELD } from '@kbn/discover-utils';
import { TRANSACTION_DURATION_FIELD, TRANSACTION_NAME_FIELD } from '@kbn/discover-utils';
jest.mock('../../../../../plugin', () => ({
getUnifiedDocViewerServices: jest.fn(),
@ -43,9 +43,11 @@ const mockAddDanger = jest.fn();
},
});
const lastValueFromMock = lastValueFrom as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
(lastValueFrom as jest.Mock).mockReset();
lastValueFromMock.mockReset();
});
describe('useTransaction hook', () => {
@ -56,7 +58,7 @@ describe('useTransaction hook', () => {
);
it('should start with loading true and transaction as null', async () => {
(lastValueFrom as jest.Mock).mockResolvedValue({});
lastValueFromMock.mockResolvedValue({});
const { result } = renderHook(() => useTransactionContext(), { wrapper });
@ -66,10 +68,18 @@ describe('useTransaction hook', () => {
it('should update transaction when data is fetched successfully', async () => {
const transactionName = 'Test Transaction';
(lastValueFrom as jest.Mock).mockResolvedValue({
const transactionDuration = 1;
lastValueFromMock.mockResolvedValue({
rawResponse: {
hits: {
hits: [{ fields: { [TRANSACTION_NAME_FIELD]: transactionName } }],
hits: [
{
fields: {
[TRANSACTION_NAME_FIELD]: transactionName,
[TRANSACTION_DURATION_FIELD]: transactionDuration,
},
},
],
},
},
});
@ -80,19 +90,20 @@ describe('useTransaction hook', () => {
expect(result.current.loading).toBe(false);
expect(result.current.transaction?.name).toBe(transactionName);
expect(result.current.transaction?.duration).toBe(transactionDuration);
expect(lastValueFrom).toHaveBeenCalledTimes(1);
});
it('should handle errors and set transaction.name as empty string, and show a toast error', async () => {
it('should handle errors and set transaction to null, and show a toast error', async () => {
const errorMessage = 'Search error';
(lastValueFrom as jest.Mock).mockRejectedValue(new Error(errorMessage));
lastValueFromMock.mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useTransactionContext(), { wrapper });
await waitFor(() => !result.current.loading);
expect(result.current.loading).toBe(false);
expect(result.current.transaction).toEqual({ name: '' });
expect(result.current.transaction).toBeNull();
expect(lastValueFrom).toHaveBeenCalledTimes(1);
expect(mockAddDanger).toHaveBeenCalledWith(
expect.objectContaining({
@ -102,7 +113,7 @@ describe('useTransaction hook', () => {
);
});
it('should set transaction.name as empty string and stop loading when transactionId is not provided', async () => {
it('should set transaction to null and stop loading when transactionId is not provided', async () => {
const wrapperWithoutTransactionId = ({ children }: { children: React.ReactNode }) => (
<TransactionProvider transactionId={undefined} indexPattern="test-index">
{children}
@ -116,7 +127,7 @@ describe('useTransaction hook', () => {
await waitFor(() => !result.current.loading);
expect(result.current.loading).toBe(false);
expect(result.current.transaction).toEqual({ name: '' });
expect(result.current.transaction).toBeNull();
expect(lastValueFrom).not.toHaveBeenCalled();
});
});

View file

@ -14,6 +14,7 @@ import { lastValueFrom } from 'rxjs';
import { i18n } from '@kbn/i18n';
import {
PROCESSOR_EVENT_FIELD,
TRANSACTION_DURATION_FIELD,
TRANSACTION_ID_FIELD,
TRANSACTION_NAME_FIELD,
} from '@kbn/discover-utils';
@ -45,7 +46,7 @@ async function getTransactionData({
size: 1,
body: {
timeout: '20s',
fields: [TRANSACTION_NAME_FIELD],
fields: [TRANSACTION_NAME_FIELD, TRANSACTION_DURATION_FIELD],
query: {
bool: {
must: [
@ -70,18 +71,19 @@ async function getTransactionData({
);
}
export interface TransactionWithOnlyName {
export interface Transaction {
name: string;
duration: number;
}
const useTransaction = ({ transactionId, indexPattern }: UseTransactionPrams) => {
const { data, core } = getUnifiedDocViewerServices();
const [transaction, setTransaction] = useState<TransactionWithOnlyName | null>(null);
const [transaction, setTransaction] = useState<Transaction | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (!transactionId) {
setTransaction({ name: '' });
setTransaction(null);
setLoading(false);
return;
}
@ -94,8 +96,14 @@ const useTransaction = ({ transactionId, indexPattern }: UseTransactionPrams) =>
setLoading(true);
const result = await getTransactionData({ transactionId, indexPattern, data, signal });
const transactionName = result.rawResponse.hits.hits[0]?.fields?.[TRANSACTION_NAME_FIELD];
setTransaction(transactionName ? { name: transactionName } : null);
const fields = result.rawResponse.hits.hits[0]?.fields;
const transactionName = fields?.[TRANSACTION_NAME_FIELD];
const transactionDuration = fields?.[TRANSACTION_DURATION_FIELD];
setTransaction({
name: transactionName || null,
duration: transactionDuration || null,
});
} catch (err) {
if (!signal.aborted) {
const error = err as Error;
@ -105,7 +113,7 @@ const useTransaction = ({ transactionId, indexPattern }: UseTransactionPrams) =>
}),
text: error.message,
});
setTransaction({ name: '' });
setTransaction(null);
}
} finally {
setLoading(false);

View file

@ -11,7 +11,6 @@ import {
HTTP_RESPONSE_STATUS_CODE_FIELD,
SERVICE_NAME_FIELD,
SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
SPAN_DURATION_FIELD,
SPAN_NAME_FIELD,
SPAN_SUBTYPE_FIELD,
SPAN_TYPE_FIELD,
@ -27,7 +26,6 @@ export const spanFields = [
TRACE_ID_FIELD,
SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
TIMESTAMP_FIELD,
SPAN_DURATION_FIELD,
HTTP_RESPONSE_STATUS_CODE_FIELD,
SPAN_TYPE_FIELD,
SPAN_SUBTYPE_FIELD,

View file

@ -12,7 +12,6 @@ import {
SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
TraceDocumentOverview,
SERVICE_ENVIRONMENT_FIELD,
SPAN_DURATION_FIELD,
SPAN_SUBTYPE_FIELD,
SPAN_TYPE_FIELD,
} from '@kbn/discover-utils';
@ -24,7 +23,6 @@ import {
FieldConfiguration,
getCommonFieldConfiguration,
} from '../../resources/get_field_configuration';
import { asDuration } from '../../utils';
export const getSpanFieldConfiguration = (
attributes: TraceDocumentOverview
@ -38,13 +36,6 @@ export const getSpanFieldConfiguration = (
content: (value) => <EuiText size="xs">{value}</EuiText>,
value: attributes[SPAN_NAME_FIELD],
},
[SPAN_DURATION_FIELD]: {
title: i18n.translate('unifiedDocViewer.observability.traces.details.spanDuration.title', {
defaultMessage: 'Duration',
}),
content: (value) => <EuiText size="xs">{asDuration(value as number)}</EuiText>,
value: attributes[SPAN_DURATION_FIELD] ?? 0,
},
[SPAN_DESTINATION_SERVICE_RESOURCE_FIELD]: {
title: i18n.translate(
'unifiedDocViewer.observability.traces.details.spanDestinationServiceResource.title',

View file

@ -11,12 +11,18 @@ import React from 'react';
import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TRANSACTION_ID_FIELD, getTraceDocumentOverview } from '@kbn/discover-utils';
import {
SPAN_DURATION_FIELD,
TRANSACTION_ID_FIELD,
getTraceDocumentOverview,
} from '@kbn/discover-utils';
import { FieldActionsProvider } from '../../../../hooks/use_field_actions';
import { TransactionProvider } from './hooks/use_transaction';
import { spanFields } from './resources/fields';
import { getSpanFieldConfiguration } from './resources/get_span_field_configuration';
import { SpanSummary } from './sub_components/span_summary';
import { SpanSummaryField } from './sub_components/span_summary_field';
import { SpanDurationSummary } from './sub_components/span_duration_summary';
export type SpanOverviewProps = DocViewRenderProps & {
transactionIndexPattern: string;
};
@ -30,10 +36,7 @@ export function SpanOverview({
transactionIndexPattern,
}: SpanOverviewProps) {
const parsedDoc = getTraceDocumentOverview(hit);
const detailTitle = i18n.translate('unifiedDocViewer.observability.traces.spanOverview.title', {
defaultMessage: 'Span detail',
});
const spanDuration = parsedDoc[SPAN_DURATION_FIELD];
return (
<TransactionProvider
@ -49,20 +52,31 @@ export function SpanOverview({
<EuiPanel color="transparent" hasShadow={false} paddingSize="none">
<EuiSpacer size="m" />
<EuiTitle size="s">
<h2>{detailTitle}</h2>
<h2>
{i18n.translate('unifiedDocViewer.observability.traces.spanOverview.title', {
defaultMessage: 'Span detail',
})}
</h2>
</EuiTitle>
<EuiSpacer size="m" />
{spanFields.map((fieldId) => {
const fieldConfiguration = getSpanFieldConfiguration(parsedDoc)[fieldId];
return (
<SpanSummary
<SpanSummaryField
key={fieldId}
fieldId={fieldId}
fieldConfiguration={fieldConfiguration}
/>
);
})}
{spanDuration && (
<>
<EuiSpacer size="m" />
<SpanDurationSummary duration={spanDuration} />
</>
)}
</EuiPanel>
</FieldActionsProvider>
</TransactionProvider>

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Duration } from '@kbn/apm-ui-shared';
import { useTransactionContext } from '../../hooks/use_transaction';
export interface SpanDurationSummaryProps {
duration: number;
}
export function SpanDurationSummary({ duration }: SpanDurationSummaryProps) {
const { transaction, loading } = useTransactionContext();
return (
<>
<EuiTitle size="s">
<h2>
{i18n.translate(
'unifiedDocViewer.observability.traces.docViewerSpanOverview.spanDurationSummary.title',
{
defaultMessage: 'Duration',
}
)}
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText color="subdued" size="xs">
{i18n.translate(
'unifiedDocViewer.observability.traces.docViewerSpanOverview.spanDurationSummary.description',
{
defaultMessage: 'Time taken to complete this span from start to finish.',
}
)}
</EuiText>
<EuiSpacer size="m" />
<>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiTitle size="xxxs">
<h3>
{i18n.translate(
'unifiedDocViewer.observability.traces.docViewerSpanOverview.spanDurationSummary.duration.title',
{
defaultMessage: 'Duration',
}
)}
</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiText size="xs">
<Duration
duration={duration}
parent={{ loading, duration: transaction?.duration, type: 'transaction' }}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
</>
</>
);
}

View file

@ -13,12 +13,12 @@ import React, { useState, useEffect } from 'react';
import { FieldWithActions } from '../../components/field_with_actions/field_with_actions';
import { useTransactionContext } from '../hooks/use_transaction';
import { FieldConfiguration } from '../../resources/get_field_configuration';
export interface SpanSummaryProps {
export interface SpanSummaryFieldProps {
fieldId: string;
fieldConfiguration: FieldConfiguration;
}
export function SpanSummary({ fieldConfiguration, fieldId }: SpanSummaryProps) {
export function SpanSummaryField({ fieldConfiguration, fieldId }: SpanSummaryFieldProps) {
const { transaction, loading } = useTransactionContext();
const [fieldValue, setFieldValue] = useState(fieldConfiguration.value);
const isTransactionNameField = fieldId === TRANSACTION_NAME_FIELD;

View file

@ -0,0 +1,114 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { lastValueFrom } from 'rxjs';
import { getUnifiedDocViewerServices } from '../../../../../plugin';
import { RootTransactionProvider, useRootTransactionContext } from './use_root_transaction';
import { TRANSACTION_DURATION_FIELD, TRANSACTION_NAME_FIELD } from '@kbn/discover-utils';
jest.mock('../../../../../plugin', () => ({
getUnifiedDocViewerServices: jest.fn(),
}));
jest.mock('rxjs', () => {
const originalModule = jest.requireActual('rxjs');
return {
...originalModule,
lastValueFrom: jest.fn(),
};
});
const mockSearch = jest.fn();
const mockAddDanger = jest.fn();
(getUnifiedDocViewerServices as jest.Mock).mockReturnValue({
data: {
search: {
search: mockSearch,
},
},
core: {
notifications: {
toasts: {
addDanger: mockAddDanger,
},
},
},
});
const lastValueFromMock = lastValueFrom as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
lastValueFromMock.mockReset();
});
describe('useRootTransaction hook', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<RootTransactionProvider traceId="test-trace" indexPattern="test-index">
{children}
</RootTransactionProvider>
);
it('should start with loading true and transaction as null', async () => {
lastValueFromMock.mockResolvedValue({});
const { result } = renderHook(() => useRootTransactionContext(), { wrapper });
expect(result.current.loading).toBe(true);
expect(lastValueFrom).toHaveBeenCalledTimes(1);
});
it('should update transaction when data is fetched successfully', async () => {
const transactionName = 'Test Transaction';
const transactionDuration = 1;
lastValueFromMock.mockResolvedValue({
rawResponse: {
hits: {
hits: [
{
fields: {
[TRANSACTION_NAME_FIELD]: transactionName,
[TRANSACTION_DURATION_FIELD]: transactionDuration,
},
},
],
},
},
});
const { result } = renderHook(() => useRootTransactionContext(), { wrapper });
await waitFor(() => !result.current.loading);
expect(result.current.loading).toBe(false);
expect(result.current.transaction?.duration).toBe(transactionDuration);
expect(lastValueFrom).toHaveBeenCalledTimes(1);
});
it('should handle errors and set transaction to null, and show a toast error', async () => {
const errorMessage = 'Search error';
lastValueFromMock.mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useRootTransactionContext(), { wrapper });
await waitFor(() => !result.current.loading);
expect(result.current.loading).toBe(false);
expect(result.current.transaction).toBeNull();
expect(lastValueFrom).toHaveBeenCalledTimes(1);
expect(mockAddDanger).toHaveBeenCalledWith(
expect.objectContaining({
title: 'An error occurred while fetching the transaction',
text: errorMessage,
})
);
});
});

View file

@ -0,0 +1,129 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import createContainer from 'constate';
import { useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { PARENT_ID_FIELD, TRACE_ID_FIELD, TRANSACTION_DURATION_FIELD } from '@kbn/discover-utils';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { lastValueFrom } from 'rxjs';
import { getUnifiedDocViewerServices } from '../../../../../plugin';
interface UseRootTransactionParams {
traceId: string;
indexPattern: string;
}
interface GetRootTransactionParams {
data: DataPublicPluginStart;
signal: AbortSignal;
traceId: string;
indexPattern: string;
}
async function getRootTransaction({
data,
signal,
traceId,
indexPattern,
}: GetRootTransactionParams) {
return lastValueFrom(
data.search.search(
{
params: {
index: indexPattern,
size: 1,
body: {
timeout: '20s',
fields: [TRANSACTION_DURATION_FIELD],
query: {
bool: {
should: [
{
constant_score: {
filter: {
bool: {
must_not: { exists: { field: PARENT_ID_FIELD } },
},
},
},
},
],
filter: [{ term: { [TRACE_ID_FIELD]: traceId } }],
},
},
},
},
},
{ abortSignal: signal }
)
);
}
export interface Transaction {
duration: number;
}
const useRootTransaction = ({ traceId, indexPattern }: UseRootTransactionParams) => {
const { core, data } = getUnifiedDocViewerServices();
const [transaction, setTransaction] = useState<Transaction | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (!traceId) {
setTransaction(null);
setLoading(false);
return;
}
const controller = new AbortController();
const { signal } = controller;
const fetchData = async () => {
try {
setLoading(true);
const result = await getRootTransaction({ data, signal, traceId, indexPattern });
const fields = result.rawResponse.hits.hits[0]?.fields;
const transactionDuration = fields?.[TRANSACTION_DURATION_FIELD];
setTransaction({
duration: transactionDuration || null,
});
} catch (err) {
if (!signal.aborted) {
const error = err as Error;
core.notifications.toasts.addDanger({
title: i18n.translate(
'unifiedDocViewer.docViewerSpanOverview.useRootTransaction.error',
{
defaultMessage: 'An error occurred while fetching the transaction',
}
),
text: error.message,
});
setTransaction(null);
}
} finally {
setLoading(false);
}
};
fetchData();
return function onUnmount() {
controller.abort();
};
}, [data, core.notifications.toasts, traceId, indexPattern]);
return { loading, transaction };
};
export const [RootTransactionProvider, useRootTransactionContext] =
createContainer(useRootTransaction);

View file

@ -12,7 +12,6 @@ import {
SERVICE_NAME_FIELD,
TIMESTAMP_FIELD,
TRACE_ID_FIELD,
TRANSACTION_DURATION_FIELD,
TRANSACTION_NAME_FIELD,
USER_AGENT_NAME_FIELD,
USER_AGENT_VERSION_FIELD,
@ -23,7 +22,6 @@ export const transactionFields = [
SERVICE_NAME_FIELD,
TRACE_ID_FIELD,
TIMESTAMP_FIELD,
TRANSACTION_DURATION_FIELD,
HTTP_RESPONSE_STATUS_CODE_FIELD,
USER_AGENT_NAME_FIELD,
USER_AGENT_VERSION_FIELD,

View file

@ -8,7 +8,6 @@
*/
import {
TRANSACTION_DURATION_FIELD,
USER_AGENT_NAME_FIELD,
TraceDocumentOverview,
USER_AGENT_VERSION_FIELD,
@ -16,7 +15,6 @@ import {
import { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiText } from '@elastic/eui';
import { asDuration } from '../../utils';
import {
FieldConfiguration,
getCommonFieldConfiguration,
@ -27,16 +25,6 @@ export const getTransactionFieldConfiguration = (
): Record<string, FieldConfiguration> => {
return {
...getCommonFieldConfiguration(attributes),
[TRANSACTION_DURATION_FIELD]: {
title: i18n.translate(
'unifiedDocViewer.observability.traces.details.transactionDuration.title',
{
defaultMessage: 'Duration',
}
),
content: (value) => <EuiText size="xs">{asDuration(value as number)}</EuiText>,
value: attributes[TRANSACTION_DURATION_FIELD] ?? 0,
},
[USER_AGENT_NAME_FIELD]: {
title: i18n.translate('unifiedDocViewer.observability.traces.details.userAgent.title', {
defaultMessage: 'User agent',

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Duration } from '@kbn/apm-ui-shared';
import { useRootTransactionContext } from '../../hooks/use_root_transaction';
export interface TransactionDurationSummaryProps {
duration: number;
}
export function TransactionDurationSummary({ duration }: TransactionDurationSummaryProps) {
const { transaction, loading } = useRootTransactionContext();
return (
<>
<EuiTitle size="s">
<h2>
{i18n.translate(
'unifiedDocViewer.observability.traces.docViewerTransactionOverview.spanDurationSummary.title',
{
defaultMessage: 'Duration',
}
)}
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText color="subdued" size="xs">
{i18n.translate(
'unifiedDocViewer.observability.traces.docViewerTransactionOverview.spanDurationSummary.description',
{
defaultMessage: 'Time taken to complete this transaction from start to finish.',
}
)}
</EuiText>
<EuiSpacer size="m" />
<>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiTitle size="xxxs">
<h3>
{i18n.translate(
'unifiedDocViewer.observability.traces.docViewerTransactionOverview.spanDurationSummary.duration.title',
{
defaultMessage: 'Duration',
}
)}
</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiText size="xs">
<Duration
duration={duration}
parent={{ type: 'trace', duration: transaction?.duration, loading }}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
</>
</>
);
}

View file

@ -12,12 +12,15 @@ import React from 'react';
import { FieldConfiguration } from '../../resources/get_field_configuration';
import { FieldWithActions } from '../../components/field_with_actions/field_with_actions';
export interface TransactionSummaryProps {
export interface TransactionSummaryFieldProps {
fieldId: string;
fieldConfiguration: FieldConfiguration;
}
export function TransactionSummary({ fieldConfiguration, fieldId }: TransactionSummaryProps) {
export function TransactionSummaryField({
fieldConfiguration,
fieldId,
}: TransactionSummaryFieldProps) {
if (!fieldConfiguration.value) {
return null;
}

View file

@ -11,12 +11,20 @@ import React from 'react';
import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getTraceDocumentOverview } from '@kbn/discover-utils';
import {
TRACE_ID_FIELD,
TRANSACTION_DURATION_FIELD,
getTraceDocumentOverview,
} from '@kbn/discover-utils';
import { FieldActionsProvider } from '../../../../hooks/use_field_actions';
import { transactionFields } from './resources/fields';
import { getTransactionFieldConfiguration } from './resources/get_transaction_field_configuration';
import { TransactionSummary } from './sub_components/transaction_summary';
export type TransactionOverviewProps = DocViewRenderProps;
import { TransactionSummaryField } from './sub_components/transaction_summary_field';
import { TransactionDurationSummary } from './sub_components/transaction_duration_summary';
import { RootTransactionProvider } from './hooks/use_root_transaction';
export type TransactionOverviewProps = DocViewRenderProps & {
tracesIndexPattern: string;
};
export function TransactionOverview({
columns,
@ -24,8 +32,10 @@ export function TransactionOverview({
filter,
onAddColumn,
onRemoveColumn,
tracesIndexPattern,
}: TransactionOverviewProps) {
const parsedDoc = getTraceDocumentOverview(hit);
const transactionDuration = parsedDoc[TRANSACTION_DURATION_FIELD];
const detailTitle = i18n.translate(
'unifiedDocViewer.observability.traces.transactionOverview.title',
@ -35,30 +45,39 @@ export function TransactionOverview({
);
return (
<FieldActionsProvider
columns={columns}
filter={filter}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
>
<EuiPanel color="transparent" hasShadow={false} paddingSize="none">
<EuiSpacer size="m" />
<EuiTitle size="s">
<h2>{detailTitle}</h2>
</EuiTitle>
<EuiSpacer size="m" />
{transactionFields.map((fieldId) => {
const fieldConfiguration = getTransactionFieldConfiguration(parsedDoc)[fieldId];
<RootTransactionProvider traceId={parsedDoc[TRACE_ID_FIELD]} indexPattern={tracesIndexPattern}>
<FieldActionsProvider
columns={columns}
filter={filter}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
>
<EuiPanel color="transparent" hasShadow={false} paddingSize="none">
<EuiSpacer size="m" />
<EuiTitle size="s">
<h2>{detailTitle}</h2>
</EuiTitle>
<EuiSpacer size="m" />
{transactionFields.map((fieldId) => {
const fieldConfiguration = getTransactionFieldConfiguration(parsedDoc)[fieldId];
return (
<TransactionSummary
key={fieldId}
fieldId={fieldId}
fieldConfiguration={fieldConfiguration}
/>
);
})}
</EuiPanel>
</FieldActionsProvider>
return (
<TransactionSummaryField
key={fieldId}
fieldId={fieldId}
fieldConfiguration={fieldConfiguration}
/>
);
})}
{transactionDuration && (
<>
<EuiSpacer size="m" />
<TransactionDurationSummary duration={transactionDuration} />
</>
)}
</EuiPanel>
</FieldActionsProvider>
</RootTransactionProvider>
);
}

View file

@ -41,6 +41,7 @@
"@kbn/event-stacktrace",
"@kbn/elastic-agent-utils",
"@kbn/data-view-utils",
"@kbn/apm-ui-shared",
"@kbn/react-hooks"
],
"exclude": ["target/**/*"]

View file

@ -98,6 +98,8 @@
"@kbn/apm-synthtrace-client/*": ["src/platform/packages/shared/kbn-apm-synthtrace-client/*"],
"@kbn/apm-types": ["x-pack/platform/packages/shared/kbn-apm-types"],
"@kbn/apm-types/*": ["x-pack/platform/packages/shared/kbn-apm-types/*"],
"@kbn/apm-ui-shared": ["src/platform/packages/shared/kbn-apm-ui-shared"],
"@kbn/apm-ui-shared/*": ["src/platform/packages/shared/kbn-apm-ui-shared/*"],
"@kbn/apm-utils": ["src/platform/packages/shared/kbn-apm-utils"],
"@kbn/apm-utils/*": ["src/platform/packages/shared/kbn-apm-utils/*"],
"@kbn/app-link-test-plugin": ["src/platform/test/plugin_functional/plugins/app_link_test"],
@ -2172,9 +2174,7 @@
"@kbn/zod-helpers/*": ["src/platform/packages/shared/kbn-zod-helpers/*"],
// END AUTOMATED PACKAGE LISTING
// Allows for importing from `kibana` package for the exported types.
"@emotion/core": [
"typings/@emotion"
]
"@emotion/core": ["typings/@emotion"]
},
// Support .tsx files and transform JSX into calls to React.createElement
"jsx": "react",

View file

@ -3886,6 +3886,10 @@
version "0.0.0"
uid ""
"@kbn/apm-ui-shared@link:src/platform/packages/shared/kbn-apm-ui-shared":
version "0.0.0"
uid ""
"@kbn/apm-utils@link:src/platform/packages/shared/kbn-apm-utils":
version "0.0.0"
uid ""