From 5c950b4f3ed657584038e5ed71b4af66ad5884a2 Mon Sep 17 00:00:00 2001 From: Irene Blanco Date: Thu, 3 Apr 2025 11:37:55 +0200 Subject: [PATCH] [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 --- .github/CODEOWNERS | 1 + package.json | 1 + .../shared/kbn-apm-ui-shared/README.mdx | 19 +++ .../shared/kbn-apm-ui-shared/index.ts | 10 ++ .../shared/kbn-apm-ui-shared/jest.config.js | 15 ++ .../shared/kbn-apm-ui-shared/kibana.jsonc | 9 ++ .../shared/kbn-apm-ui-shared/package.json | 6 + .../shared/kbn-apm-ui-shared/setup_tests.ts | 11 ++ .../src/components/duration/duration.test.tsx | 61 +++++++++ .../src/components/duration/index.tsx | 41 ++++++ .../components/duration/percent_of_parent.tsx | 54 ++++++++ .../kbn-apm-ui-shared/src}/typings/common.ts | 0 .../kbn-apm-ui-shared/src}/typings/index.ts | 0 .../src}/utils/formatters/duration.test.ts | 0 .../src}/utils/formatters/duration.ts | 10 +- .../src}/utils/formatters/index.ts | 0 .../src}/utils/formatters/numeric.test.ts | 29 +++- .../src}/utils/formatters/numeric.ts | 20 ++- .../kbn-apm-ui-shared/src}/utils/index.ts | 0 .../shared/kbn-apm-ui-shared/tsconfig.json | 12 ++ .../span_document_profile/profile.test.ts | 13 +- .../accessors/doc_viewer.tsx | 11 +- .../accessors/index.ts | 2 +- .../profile.test.ts | 10 +- .../transaction_document_profile/profile.ts | 56 ++++---- .../register_profile_providers.ts | 2 +- .../hooks/use_transaction.test.tsx | 31 +++-- .../hooks/use_transaction.ts | 22 ++- .../resources/fields.ts | 2 - .../get_span_field_configuration.tsx | 9 -- .../span_overview.tsx | 30 ++-- .../span_duration_summary/index.tsx | 85 ++++++++++++ ...pan_summary.tsx => span_summary_field.tsx} | 4 +- .../hooks/use_root_transaction.test.tsx | 114 ++++++++++++++++ .../hooks/use_root_transaction.ts | 129 ++++++++++++++++++ .../resources/fields.ts | 2 - .../get_transaction_field_configuration.tsx | 12 -- .../transaction_duration_summary/index.tsx | 85 ++++++++++++ ...mary.tsx => transaction_summary_field.tsx} | 7 +- .../transaction_overview.tsx | 73 ++++++---- .../shared/unified_doc_viewer/tsconfig.json | 1 + tsconfig.base.json | 6 +- yarn.lock | 4 + 43 files changed, 876 insertions(+), 133 deletions(-) create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/README.mdx create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/index.ts create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/jest.config.js create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/kibana.jsonc create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/package.json create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/setup_tests.ts create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/duration.test.tsx create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/index.tsx create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/percent_of_parent.tsx rename src/platform/{plugins/shared/unified_doc_viewer/public/components/observability/traces => packages/shared/kbn-apm-ui-shared/src}/typings/common.ts (100%) rename src/platform/{plugins/shared/unified_doc_viewer/public/components/observability/traces => packages/shared/kbn-apm-ui-shared/src}/typings/index.ts (100%) rename src/platform/{plugins/shared/unified_doc_viewer/public/components/observability/traces => packages/shared/kbn-apm-ui-shared/src}/utils/formatters/duration.test.ts (100%) rename src/platform/{plugins/shared/unified_doc_viewer/public/components/observability/traces => packages/shared/kbn-apm-ui-shared/src}/utils/formatters/duration.ts (89%) rename src/platform/{plugins/shared/unified_doc_viewer/public/components/observability/traces => packages/shared/kbn-apm-ui-shared/src}/utils/formatters/index.ts (100%) rename src/platform/{plugins/shared/unified_doc_viewer/public/components/observability/traces => packages/shared/kbn-apm-ui-shared/src}/utils/formatters/numeric.test.ts (80%) rename src/platform/{plugins/shared/unified_doc_viewer/public/components/observability/traces => packages/shared/kbn-apm-ui-shared/src}/utils/formatters/numeric.ts (72%) rename src/platform/{plugins/shared/unified_doc_viewer/public/components/observability/traces => packages/shared/kbn-apm-ui-shared/src}/utils/index.ts (100%) create mode 100644 src/platform/packages/shared/kbn-apm-ui-shared/tsconfig.json create mode 100644 src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_duration_summary/index.tsx rename src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/{span_summary.tsx => span_summary_field.tsx} (94%) create mode 100644 src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/hooks/use_root_transaction.test.tsx create mode 100644 src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/hooks/use_root_transaction.ts create mode 100644 src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_duration_summary/index.tsx rename src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/{transaction_summary.tsx => transaction_summary_field.tsx} (89%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8b74bc2ff549..69b6be5df2e8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/package.json b/package.json index e873c21b69ea..0a8de28e6115 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/README.mdx b/src/platform/packages/shared/kbn-apm-ui-shared/README.mdx new file mode 100644 index 000000000000..9750090cf526 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/README.mdx @@ -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. | diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/index.ts b/src/platform/packages/shared/kbn-apm-ui-shared/index.ts new file mode 100644 index 000000000000..051875c324bd --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/index.ts @@ -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'; diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/jest.config.js b/src/platform/packages/shared/kbn-apm-ui-shared/jest.config.js new file mode 100644 index 000000000000..fabe44ce37f4 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/jest.config.js @@ -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: ['/src/platform/packages/shared/kbn-apm-ui-shared'], + setupFilesAfterEnv: ['/src/platform/packages/shared/kbn-apm-ui-shared/setup_tests.ts'], +}; diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/kibana.jsonc b/src/platform/packages/shared/kbn-apm-ui-shared/kibana.jsonc new file mode 100644 index 000000000000..0dd216365da9 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/kibana.jsonc @@ -0,0 +1,9 @@ +{ + "type": "shared-browser", + "id": "@kbn/apm-ui-shared", + "owner": [ + "@elastic/obs-ux-infra_services-team" + ], + "group": "platform", + "visibility": "shared" +} \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/package.json b/src/platform/packages/shared/kbn-apm-ui-shared/package.json new file mode 100644 index 000000000000..7f873d9de912 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/package.json @@ -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" +} \ No newline at end of file diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/setup_tests.ts b/src/platform/packages/shared/kbn-apm-ui-shared/setup_tests.ts new file mode 100644 index 000000000000..5ebc6d3dac1c --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/setup_tests.ts @@ -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'; diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/duration.test.tsx b/src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/duration.test.tsx new file mode 100644 index 000000000000..d2ed3774463c --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/duration.test.tsx @@ -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(); + 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(); + 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(); + expect( + screen.getByText( + `${expectedDurationText} ${getExpectedParentDurationText(parentWithLoading.type)}` + ) + ).toBeInTheDocument(); + expect(screen.queryByTestId(loadingDataTestSubj)).not.toBeInTheDocument(); + }); + }); + }); +}); diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/index.tsx b/src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/index.tsx new file mode 100644 index 000000000000..5cf83aa242d7 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/index.tsx @@ -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) { + {asDuration(duration)}; + } + return ( + + {asDuration(duration)}   + {parent?.loading && } + {!parent?.loading && parent?.duration && ( + + )} + + ); +} diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/percent_of_parent.tsx b/src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/percent_of_parent.tsx new file mode 100644 index 000000000000..f06c88c93410 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/src/components/duration/percent_of_parent.tsx @@ -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 ? ( + + <>{percentOfParentText} + + ) : ( + percentOfParentText + )} + + ); +} diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/typings/common.ts b/src/platform/packages/shared/kbn-apm-ui-shared/src/typings/common.ts similarity index 100% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/typings/common.ts rename to src/platform/packages/shared/kbn-apm-ui-shared/src/typings/common.ts diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/typings/index.ts b/src/platform/packages/shared/kbn-apm-ui-shared/src/typings/index.ts similarity index 100% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/typings/index.ts rename to src/platform/packages/shared/kbn-apm-ui-shared/src/typings/index.ts diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/duration.test.ts b/src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/duration.test.ts similarity index 100% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/duration.test.ts rename to src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/duration.test.ts diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/duration.ts b/src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/duration.ts similarity index 89% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/duration.ts rename to src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/duration.ts index 6c1f9b651170..18a4f15922e0 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/duration.ts +++ b/src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/duration.ts @@ -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), diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/index.ts b/src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/index.ts similarity index 100% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/index.ts rename to src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/index.ts diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/numeric.test.ts b/src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/numeric.test.ts similarity index 80% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/numeric.test.ts rename to src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/numeric.test.ts index 96057746e5fb..80b1dbec4218 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/numeric.test.ts +++ b/src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/numeric.test.ts @@ -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'); + }); + }); }); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/numeric.ts b/src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/numeric.ts similarity index 72% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/numeric.ts rename to src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/numeric.ts index 5e8cf22ea33b..0b57d13cea7e 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/formatters/numeric.ts +++ b/src/platform/packages/shared/kbn-apm-ui-shared/src/utils/formatters/numeric.ts @@ -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, threshold = 10) { } return asDecimal(value); } + +export function asPercent( + numerator: Maybe, + 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%'); +} diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/index.ts b/src/platform/packages/shared/kbn-apm-ui-shared/src/utils/index.ts similarity index 100% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/utils/index.ts rename to src/platform/packages/shared/kbn-apm-ui-shared/src/utils/index.ts diff --git a/src/platform/packages/shared/kbn-apm-ui-shared/tsconfig.json b/src/platform/packages/shared/kbn-apm-ui-shared/tsconfig.json new file mode 100644 index 000000000000..603634e5f8ee --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-ui-shared/tsconfig.json @@ -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", + ] +} diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/profile.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/profile.test.ts index 2aa7999bc597..34df699778b9 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/profile.test.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/profile.test.ts @@ -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); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/doc_viewer.tsx b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/doc_viewer.tsx index c5b74e6e876e..9c5164e9927a 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/doc_viewer.tsx +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/doc_viewer.tsx @@ -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 ; + return ( + + ); }, }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/index.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/index.ts index 09a3c82b8ad1..07c68375c440 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/index.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/index.ts @@ -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'; diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.test.ts index f7f131d0e462..c9258e3d92e1 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.test.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.test.ts @@ -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( diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.ts index df5756ffd19f..46b6e1d4bc81 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.ts @@ -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); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/register_profile_providers.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/register_profile_providers.ts index 465b4c2ba1c3..9b1062983d50 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/register_profile_providers.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/register_profile_providers.ts @@ -151,5 +151,5 @@ const createDocumentProfileProviders = (providerServices: ProfileProviderService createExampleDocumentProfileProvider(), createObservabilityLogDocumentProfileProvider(providerServices), createObservabilityTracesSpanDocumentProfileProvider(providerServices), - createObservabilityTracesTransactionDocumentProfileProvider(), + createObservabilityTracesTransactionDocumentProfileProvider(providerServices), ]; diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/hooks/use_transaction.test.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/hooks/use_transaction.test.tsx index 87aa9a45668e..b5aa5122f187 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/hooks/use_transaction.test.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/hooks/use_transaction.test.tsx @@ -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 }) => ( {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(); }); }); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/hooks/use_transaction.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/hooks/use_transaction.ts index 05e0cc7f7212..1cf96cf22c89 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/hooks/use_transaction.ts +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/hooks/use_transaction.ts @@ -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(null); + const [transaction, setTransaction] = useState(null); const [loading, setLoading] = useState(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); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/fields.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/fields.ts index 63b18a6912e9..3e5e3127cedb 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/fields.ts +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/fields.ts @@ -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, diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/get_span_field_configuration.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/get_span_field_configuration.tsx index ed30a2546c3d..25686f138609 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/get_span_field_configuration.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/resources/get_span_field_configuration.tsx @@ -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) => {value}, value: attributes[SPAN_NAME_FIELD], }, - [SPAN_DURATION_FIELD]: { - title: i18n.translate('unifiedDocViewer.observability.traces.details.spanDuration.title', { - defaultMessage: 'Duration', - }), - content: (value) => {asDuration(value as number)}, - value: attributes[SPAN_DURATION_FIELD] ?? 0, - }, [SPAN_DESTINATION_SERVICE_RESOURCE_FIELD]: { title: i18n.translate( 'unifiedDocViewer.observability.traces.details.spanDestinationServiceResource.title', diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx index a529a479192d..5727d2596145 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx @@ -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 ( -

{detailTitle}

+

+ {i18n.translate('unifiedDocViewer.observability.traces.spanOverview.title', { + defaultMessage: 'Span detail', + })} +

{spanFields.map((fieldId) => { const fieldConfiguration = getSpanFieldConfiguration(parsedDoc)[fieldId]; return ( - ); })} + + {spanDuration && ( + <> + + + + )}
diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_duration_summary/index.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_duration_summary/index.tsx new file mode 100644 index 000000000000..4310e3b063dc --- /dev/null +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_duration_summary/index.tsx @@ -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 ( + <> + +

+ {i18n.translate( + 'unifiedDocViewer.observability.traces.docViewerSpanOverview.spanDurationSummary.title', + { + defaultMessage: 'Duration', + } + )} +

+
+ + + {i18n.translate( + 'unifiedDocViewer.observability.traces.docViewerSpanOverview.spanDurationSummary.description', + { + defaultMessage: 'Time taken to complete this span from start to finish.', + } + )} + + + + <> + + + + + +

+ {i18n.translate( + 'unifiedDocViewer.observability.traces.docViewerSpanOverview.spanDurationSummary.duration.title', + { + defaultMessage: 'Duration', + } + )} +

+
+
+
+
+ + + + + + +
+ + + + ); +} diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary_field.tsx similarity index 94% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary.tsx rename to src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary_field.tsx index a484c6d54cbc..dd8fd6774543 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/sub_components/span_summary_field.tsx @@ -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; diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/hooks/use_root_transaction.test.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/hooks/use_root_transaction.test.tsx new file mode 100644 index 000000000000..919ba406170f --- /dev/null +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/hooks/use_root_transaction.test.tsx @@ -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 }) => ( + + {children} + + ); + + 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, + }) + ); + }); +}); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/hooks/use_root_transaction.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/hooks/use_root_transaction.ts new file mode 100644 index 000000000000..7e39a37d1cb9 --- /dev/null +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/hooks/use_root_transaction.ts @@ -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(null); + const [loading, setLoading] = useState(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); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/fields.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/fields.ts index 1821c2a20f39..a661378179f8 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/fields.ts +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/fields.ts @@ -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, diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/get_transaction_field_configuration.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/get_transaction_field_configuration.tsx index 94fc960424a6..b5b48fe2bdcd 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/get_transaction_field_configuration.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/resources/get_transaction_field_configuration.tsx @@ -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 => { return { ...getCommonFieldConfiguration(attributes), - [TRANSACTION_DURATION_FIELD]: { - title: i18n.translate( - 'unifiedDocViewer.observability.traces.details.transactionDuration.title', - { - defaultMessage: 'Duration', - } - ), - content: (value) => {asDuration(value as number)}, - value: attributes[TRANSACTION_DURATION_FIELD] ?? 0, - }, [USER_AGENT_NAME_FIELD]: { title: i18n.translate('unifiedDocViewer.observability.traces.details.userAgent.title', { defaultMessage: 'User agent', diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_duration_summary/index.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_duration_summary/index.tsx new file mode 100644 index 000000000000..6884b408603c --- /dev/null +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_duration_summary/index.tsx @@ -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 ( + <> + +

+ {i18n.translate( + 'unifiedDocViewer.observability.traces.docViewerTransactionOverview.spanDurationSummary.title', + { + defaultMessage: 'Duration', + } + )} +

+
+ + + {i18n.translate( + 'unifiedDocViewer.observability.traces.docViewerTransactionOverview.spanDurationSummary.description', + { + defaultMessage: 'Time taken to complete this transaction from start to finish.', + } + )} + + + + <> + + + + + +

+ {i18n.translate( + 'unifiedDocViewer.observability.traces.docViewerTransactionOverview.spanDurationSummary.duration.title', + { + defaultMessage: 'Duration', + } + )} +

+
+
+
+
+ + + + + + +
+ + + + ); +} diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary_field.tsx similarity index 89% rename from src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary.tsx rename to src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary_field.tsx index 8a318fa73f2c..e2c8b5fab66b 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/sub_components/transaction_summary_field.tsx @@ -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; } diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx index a155cd8dc578..9109e2aa4159 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx @@ -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 ( - - - - -

{detailTitle}

-
- - {transactionFields.map((fieldId) => { - const fieldConfiguration = getTransactionFieldConfiguration(parsedDoc)[fieldId]; + + + + + +

{detailTitle}

+
+ + {transactionFields.map((fieldId) => { + const fieldConfiguration = getTransactionFieldConfiguration(parsedDoc)[fieldId]; - return ( - - ); - })} -
-
+ return ( + + ); + })} + + {transactionDuration && ( + <> + + + + )} +
+
+ ); } diff --git a/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json b/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json index ed894a82cac0..7c44777a562a 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json +++ b/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json @@ -41,6 +41,7 @@ "@kbn/event-stacktrace", "@kbn/elastic-agent-utils", "@kbn/data-view-utils", + "@kbn/apm-ui-shared", "@kbn/react-hooks" ], "exclude": ["target/**/*"] diff --git a/tsconfig.base.json b/tsconfig.base.json index 279a9c8ced5d..0ceca4e9c1fb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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", diff --git a/yarn.lock b/yarn.lock index cb46fdd3f3b3..ef64cd9d09e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 ""