diff --git a/src/platform/plugins/shared/discover_shared/public/services/discover_features/types.ts b/src/platform/plugins/shared/discover_shared/public/services/discover_features/types.ts index 593fe0970ffc9..583116f6472b4 100644 --- a/src/platform/plugins/shared/discover_shared/public/services/discover_features/types.ts +++ b/src/platform/plugins/shared/discover_shared/public/services/discover_features/types.ts @@ -16,9 +16,10 @@ import type { ErrorsByTraceId, TraceRootSpan, UnifiedSpanDocument, + FocusedTraceWaterfallProps, + FullTraceWaterfallProps, } from '@kbn/apm-types'; -import type { ProcessorEvent } from '@kbn/apm-types-shared'; -import type { HistogramItem } from '@kbn/apm-types-shared'; +import type { HistogramItem, ProcessorEvent } from '@kbn/apm-types-shared'; import type { DataView } from '@kbn/data-views-plugin/common'; import type React from 'react'; import type { IndicatorType } from '@kbn/slo-schema'; @@ -126,6 +127,16 @@ export type SecuritySolutionFeature = /** **************** Observability Traces ****************/ +interface ObservabilityFocusedTraceWaterfallFeature { + id: 'observability-focused-trace-waterfall'; + render: (props: FocusedTraceWaterfallProps) => JSX.Element; +} + +interface ObservabilityFullTraceWaterfallFeature { + id: 'observability-full-trace-waterfall'; + render: (props: FullTraceWaterfallProps) => JSX.Element; +} + export interface ObservabilityTracesSpanLinksFeature { id: 'observability-traces-fetch-span-links'; fetchSpanLinks: ( @@ -224,7 +235,9 @@ export type ObservabilityTracesFeature = | ObservabilityTracesFetchRootSpanByTraceIdFeature | ObservabilityTracesFetchSpanFeature | ObservabilityTracesFetchLatencyOverallTransactionDistributionFeature - | ObservabilityTracesFetchLatencyOverallSpanDistributionFeature; + | ObservabilityTracesFetchLatencyOverallSpanDistributionFeature + | ObservabilityFocusedTraceWaterfallFeature + | ObservabilityFullTraceWaterfallFeature; /** ****************************************************************************************/ diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.test.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.test.tsx index 00f9daa373de7..4f2903ea2061c 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.test.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.test.tsx @@ -7,33 +7,12 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; -import { render, screen, act, waitFor } from '@testing-library/react'; -import { - FullScreenWaterfall, - type FullScreenWaterfallProps, - EUI_FLYOUT_BODY_OVERFLOW_CLASS, -} from '.'; import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; - -let capturedCallbacks: any = null; - -jest.mock('@kbn/embeddable-plugin/public', () => ({ - EmbeddableRenderer: ({ type, getParentApi, hidePanelChrome }: any) => { - const api = getParentApi(); - capturedCallbacks = api.getSerializedStateForChild(); - - return ( -
- Embeddable Renderer Mock -
- ); - }, -})); +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { FullScreenWaterfall, type FullScreenWaterfallProps } from '.'; +import { setUnifiedDocViewerServices } from '../../../../../plugin'; +import type { UnifiedDocViewerServices } from '../../../../../types'; jest.mock('./waterfall_flyout/span_flyout', () => ({ SpanFlyout: ({ traceId, spanId, _, activeSection }: any) => ( @@ -62,30 +41,22 @@ describe('FullScreenWaterfall', () => { onExitFullScreen: jest.fn(), }; - beforeEach(() => { - jest.clearAllMocks(); - capturedCallbacks = null; + beforeAll(() => { + setUnifiedDocViewerServices({ + discoverShared: { + features: { + registry: { + getById: () => ({ + render: () =>
FullTraceWaterfall
, + }), + }, + }, + }, + } as unknown as UnifiedDocViewerServices); }); - it('should render APM trace waterfall embeddable with hidden chrome', () => { - render(); - - const embeddable = screen.getByTestId('embeddableRenderer'); - expect(embeddable).toHaveAttribute('data-type', 'APM_TRACE_WATERFALL_EMBEDDABLE'); - expect(embeddable).toHaveAttribute('data-hide-panel-chrome', 'true'); - }); - - it('wraps EmbeddableRenderer with CSS override for proper layout', () => { - const { container } = render(); - - const embeddable = container.querySelector('[data-test-subj="embeddableRenderer"]'); - expect(embeddable).toBeInTheDocument(); - - const wrapper = embeddable?.parentElement; - expect(wrapper).toHaveStyleRule('width', '100%'); - expect(wrapper).toHaveStyleRule('display', 'block!important', { - target: '.embPanel__content', - }); + beforeEach(() => { + jest.clearAllMocks(); }); it('should not display nested flyouts initially', () => { @@ -95,82 +66,17 @@ describe('FullScreenWaterfall', () => { expect(screen.queryByTestId('logsFlyout')).not.toBeInTheDocument(); }); - describe('nested flyout interactions', () => { - it('should display span details when clicking a waterfall node', () => { - render(); - - act(() => { - capturedCallbacks.onNodeClick('test-span-id'); - }); - - const spanFlyout = screen.getByTestId('spanFlyout'); - expect(spanFlyout).toHaveAttribute('data-trace-id', 'test-trace-id'); - expect(spanFlyout).toHaveAttribute('data-span-id', 'test-span-id'); - expect(spanFlyout).not.toHaveAttribute('data-active-section'); - expect(screen.queryByTestId('logsFlyout')).not.toBeInTheDocument(); - }); - - it('should display span errors table when clicking an error with multiple occurrences', () => { - render(); - - act(() => { - capturedCallbacks.onErrorClick({ - traceId: 'test-trace-id', - docId: 'test-error-doc-id', - errorCount: 5, - }); - }); - - const spanFlyout = screen.getByTestId('spanFlyout'); - expect(spanFlyout).toHaveAttribute('data-active-section', 'errors-table'); - expect(screen.queryByTestId('logsFlyout')).not.toBeInTheDocument(); - }); - - it('should display log details when clicking a single error', () => { - render(); - - act(() => { - capturedCallbacks.onErrorClick({ - traceId: 'test-trace-id', - docId: 'test-doc-id', - errorCount: 1, - errorDocId: 'test-error-log-id', - }); - }); - - expect(screen.getByTestId('logsFlyout')).toHaveAttribute('data-id', 'test-error-log-id'); - expect(screen.queryByTestId('spanFlyout')).not.toBeInTheDocument(); - }); - - it('should not open any flyout when clicking a single error without errorDocId', () => { - render(); - - act(() => { - capturedCallbacks.onErrorClick({ - traceId: 'test-trace-id', - docId: 'test-doc-id', - errorCount: 1, - }); - }); + it('should display the full trace waterfall', () => { + render(); - expect(screen.queryByTestId('spanFlyout')).not.toBeInTheDocument(); - expect(screen.queryByTestId('logsFlyout')).not.toBeInTheDocument(); - }); + expect(screen.getByTestId('fullTraceWaterfall')).toBeInTheDocument(); }); - describe('scrollElement integration', () => { - it('should pass scrollElement with correct EUI class to embeddable', async () => { - render(); - - await waitFor(() => { - expect(screen.getByTestId('embeddableRenderer')).toBeInTheDocument(); - }); + describe('when service name is undefined', () => { + it('does not display the full trace waterfall', () => { + render(); - expect(capturedCallbacks.scrollElement).not.toBeNull(); - expect(capturedCallbacks.scrollElement).toBeInstanceOf(Element); - expect( - capturedCallbacks.scrollElement.classList.contains(EUI_FLYOUT_BODY_OVERFLOW_CLASS) - ).toBe(true); + expect(screen.queryByTestId('fullTraceWaterfall')).not.toBeInTheDocument(); }); }); }); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.tsx index 389ce18c57cca..e032d163eb542 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.tsx @@ -12,19 +12,18 @@ import { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle, - useGeneratedHtmlId, useEuiTheme, + useGeneratedHtmlId, } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { EmbeddableRenderer } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; import React, { useCallback, useState } from 'react'; +import { getUnifiedDocViewerServices } from '../../../../../plugin'; import type { TraceOverviewSections } from '../../doc_viewer_overview/overview'; -import type { spanFlyoutId as spanFlyoutIdType } from './waterfall_flyout/span_flyout'; -import { SpanFlyout, spanFlyoutId } from './waterfall_flyout/span_flyout'; import type { logsFlyoutId as logsFlyoutIdType } from './waterfall_flyout/logs_flyout'; import { LogsFlyout, logsFlyoutId } from './waterfall_flyout/logs_flyout'; +import type { spanFlyoutId as spanFlyoutIdType } from './waterfall_flyout/span_flyout'; +import { SpanFlyout, spanFlyoutId } from './waterfall_flyout/span_flyout'; export const EUI_FLYOUT_BODY_OVERFLOW_CLASS = 'euiFlyoutBody__overflow'; @@ -45,6 +44,10 @@ export const FullScreenWaterfall = ({ serviceName, onExitFullScreen, }: FullScreenWaterfallProps) => { + const { discoverShared } = getUnifiedDocViewerServices(); + const FullTraceWaterfall = discoverShared.features.registry.getById( + 'observability-full-trace-waterfall' + )?.render; const { euiTheme } = useEuiTheme(); const [docId, setDocId] = useState(null); const [docIndex, setDocIndex] = useState(undefined); @@ -71,7 +74,6 @@ export const FullScreenWaterfall = ({ * Obtains the EUI flyout scroll container for the trace waterfall embeddable. * * This pattern is necessary because: - * - Embeddables are constructed once with immutable initial state * - EUI components don't expose refs, requiring a wrapper div with closest() * - scrollElement must be available before the embeddable initializes (conditional render below) * @@ -79,49 +81,12 @@ export const FullScreenWaterfall = ({ * TODO: Once the EUI team implements a scrollRef prop (or exposes refs on EUIFlyoutBody, Issue: 2564 in kibana-team repository), * we can replace this workaround with a direct ref usage. */ - const embeddableContainerRef = useCallback((node: HTMLDivElement | null) => { + const waterfallContainerRef = useCallback((node: HTMLDivElement | null) => { if (node) { setScrollElement(node.closest(`.${EUI_FLYOUT_BODY_OVERFLOW_CLASS}`) ?? null); } }, []); - const getParentApi = useCallback(() => { - return { - getSerializedStateForChild: () => ({ - traceId, - rangeFrom, - rangeTo, - serviceName, - scrollElement, - onErrorClick: (params: { - traceId: string; - docId: string; - errorCount: number; - errorDocId?: string; - docIndex?: string; - }) => { - if (params.errorCount > 1) { - setActiveFlyoutId(spanFlyoutId); - setActiveSection('errors-table'); - setDocId(params.docId); - setDocIndex(undefined); - } else if (params.errorDocId) { - setActiveFlyoutId(logsFlyoutId); - setDocId(params.errorDocId); - setDocIndex(params.docIndex); - } - }, - onNodeClick: (nodeSpanId: string) => { - setActiveSection(undefined); - setDocId(nodeSpanId); - setDocIndex(undefined); - setActiveFlyoutId(spanFlyoutId); - }, - mode: 'full', - }), - }; - }, [traceId, rangeFrom, rangeTo, serviceName, scrollElement]); - function handleCloseFlyout() { setActiveFlyoutId(null); setActiveSection(undefined); @@ -129,6 +94,36 @@ export const FullScreenWaterfall = ({ setDocIndex(undefined); } + function handleNodeClick(nodeSpanId: string) { + setActiveSection(undefined); + setDocId(nodeSpanId); + setDocIndex(undefined); + setActiveFlyoutId(spanFlyoutId); + } + + function handleErrorClick(params: { + traceId: string; + docId: string; + errorCount: number; + errorDocId?: string; + docIndex?: string; + }) { + if (params.errorCount > 1) { + setActiveFlyoutId(spanFlyoutId); + setActiveSection('errors-table'); + setDocId(params.docId); + setDocIndex(undefined); + } else if (params.errorDocId) { + setActiveFlyoutId(logsFlyoutId); + setDocId(params.errorDocId); + setDocIndex(params.docIndex); + } + } + + if (!FullTraceWaterfall) { + return null; + } + return ( - {scrollElement ? ( - + {scrollElement && serviceName ? ( + ) : null} diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_waterfall/index.test.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_waterfall/index.test.tsx deleted file mode 100644 index fc26d1e28bd68..0000000000000 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_waterfall/index.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 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 { render } from '@testing-library/react'; -import { TraceWaterfall } from '.'; -import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; - -jest.mock('../../../../../plugin', () => ({ - getUnifiedDocViewerServices: () => ({ - data: { - query: { - timefilter: { - timefilter: { - getAbsoluteTime: () => ({ from: '2024-01-01', to: '2024-01-02' }), - }, - }, - }, - }, - }), -})); - -jest.mock('@kbn/embeddable-plugin/public', () => ({ - EmbeddableRenderer: () =>
, -})); - -jest.mock('../full_screen_waterfall', () => ({ - FullScreenWaterfall: () => null, -})); - -jest.mock('./full_screen_waterfall_tour_step', () => ({ - TraceWaterfallTourStep: () => null, -})); - -jest.mock('../../../../..', () => ({ - ContentFrameworkSection: ({ children }: { children: React.ReactNode }) =>
{children}
, -})); - -describe('TraceWaterfall', () => { - const dataView = createStubDataView({ - spec: { - id: 'test-dataview', - title: 'test-pattern', - timeFieldName: '@timestamp', - }, - }); - - it('wraps EmbeddableRenderer with CSS override for proper layout', () => { - const { container } = render(); - - const embeddable = container.querySelector('[data-test-subj="embeddable-renderer"]'); - expect(embeddable).toBeInTheDocument(); - - // Verify the wrapper exists with the CSS override - const wrapper = embeddable?.parentElement; - expect(wrapper).toHaveStyleRule('width', '100%'); - expect(wrapper).toHaveStyleRule('display', 'block!important', { - target: '.embPanel__content', - }); - }); -}); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_waterfall/index.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_waterfall/index.tsx index 08aededd90cca..3d090f41007b1 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_waterfall/index.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/trace_waterfall/index.tsx @@ -7,12 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EmbeddableRenderer } from '@kbn/embeddable-plugin/public'; +import { EuiDelayRender } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; -import React, { useCallback, useState } from 'react'; -import { EuiDelayRender } from '@elastic/eui'; -import { css } from '@emotion/react'; +import React, { useState } from 'react'; import { ContentFrameworkSection } from '../../../../..'; import { getUnifiedDocViewerServices } from '../../../../../plugin'; import { FullScreenWaterfall } from '../full_screen_waterfall'; @@ -39,22 +37,16 @@ const sectionTitle = i18n.translate('unifiedDocViewer.observability.traces.trace }); export function TraceWaterfall({ traceId, docId, serviceName, dataView }: Props) { - const { data } = getUnifiedDocViewerServices(); + const { data, discoverShared } = getUnifiedDocViewerServices(); + const FocusedTraceWaterfall = discoverShared.features.registry.getById( + 'observability-focused-trace-waterfall' + )?.render; const [showFullScreenWaterfall, setShowFullScreenWaterfall] = useState(false); const { from: rangeFrom, to: rangeTo } = data.query.timefilter.timefilter.getAbsoluteTime(); - const getParentApi = useCallback( - () => ({ - getSerializedStateForChild: () => ({ - traceId, - rangeFrom, - rangeTo, - docId, - mode: 'summary', - }), - }), - [docId, rangeFrom, rangeTo, traceId] - ); + if (!FocusedTraceWaterfall) { + return null; + } const actionId = 'traceWaterfallFullScreenAction'; return ( @@ -86,25 +78,14 @@ export function TraceWaterfall({ traceId, docId, serviceName, dataView }: Props) }, ]} > - {/* TODO: This is a workaround for layout issues when using hidePanelChrome outside of Dashboard. - The PresentationPanel applies flex styles (.embPanel__content) that cause width: 0 in non-Dashboard contexts. - This should be removed once PresentationPanel properly supports hidePanelChrome as an out-of-the-box solution. - Issue: https://github.com/elastic/kibana/issues/248307 - */} -
- -
+ ) : null} void; + onErrorClick?: FullTraceWaterfallOnErrorClick; +} + +export type FullTraceWaterfallOnErrorClick = (params: { + traceId: string; + docId: string; + errorCount: number; + errorDocId?: string; + docIndex?: string; +}) => void; diff --git a/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/focused_trace_waterfall_embeddable.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/focused_trace_waterfall/focused_trace_waterfall_renderer.tsx similarity index 64% rename from x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/focused_trace_waterfall_embeddable.tsx rename to x-pack/solutions/observability/plugins/apm/public/components/shared/focused_trace_waterfall/focused_trace_waterfall_renderer.tsx index 78079947003c8..3fbc3a589a8bf 100644 --- a/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/focused_trace_waterfall_embeddable.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/focused_trace_waterfall/focused_trace_waterfall_renderer.tsx @@ -4,19 +4,25 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import React from 'react'; -import { FocusedTraceWaterfall } from '../../components/shared/focused_trace_waterfall'; -import { isPending, useFetcher } from '../../hooks/use_fetcher'; -import { Loading } from './loading'; -import type { ApmTraceWaterfallEmbeddableFocusedProps } from './react_embeddable_factory'; -export function FocusedTraceWaterfallEmbeddable({ - rangeFrom, - rangeTo, - traceId, - docId, -}: Omit) { +import { i18n } from '@kbn/i18n'; +import type { FocusedTraceWaterfallProps } from '@kbn/apm-types'; +import { EuiCallOut } from '@elastic/eui'; +import type { CoreStart } from '@kbn/core/public'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { isPending, useFetcher } from '../../../hooks/use_fetcher'; +import { FocusedTraceWaterfall } from '.'; +import { Loading } from '../trace_waterfall/loading'; +import { createCallApmApi } from '../../../services/rest/create_call_apm_api'; + +interface Props extends FocusedTraceWaterfallProps { + core: CoreStart; +} + +export function FocusedTraceWaterfallRenderer({ traceId, rangeFrom, rangeTo, docId, core }: Props) { + useEffectOnce(() => { + createCallApmApi(core); + }); const { data, status } = useFetcher( (callApmApi) => { return callApmApi('GET /internal/apm/unified_traces/{traceId}/summary', { diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/focused_trace_waterfall/lazy_create_focused_trace_waterfall_renderer.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/focused_trace_waterfall/lazy_create_focused_trace_waterfall_renderer.tsx new file mode 100644 index 0000000000000..448e89f41ee4a --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/focused_trace_waterfall/lazy_create_focused_trace_waterfall_renderer.tsx @@ -0,0 +1,23 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { dynamic } from '@kbn/shared-ux-utility'; +import type { CoreStart } from '@kbn/core/public'; +import type { FocusedTraceWaterfallProps } from '@kbn/apm-types'; + +const LazyFocusedTraceWaterfallRendererComponent = dynamic(() => + import('./focused_trace_waterfall_renderer').then((mod) => ({ + default: mod.FocusedTraceWaterfallRenderer, + })) +); + +export function createLazyFocusedTraceWaterfallRenderer({ core }: { core: CoreStart }) { + return (props: FocusedTraceWaterfallProps) => { + return ; + }; +} diff --git a/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/trace_waterfall_embeddable.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/trace_waterfall/full_trace_waterfall_renderer.tsx similarity index 72% rename from x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/trace_waterfall_embeddable.tsx rename to x-pack/solutions/observability/plugins/apm/public/components/shared/trace_waterfall/full_trace_waterfall_renderer.tsx index 1d83955991516..5e7af44a66bfb 100644 --- a/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/trace_waterfall_embeddable.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/trace_waterfall/full_trace_waterfall_renderer.tsx @@ -4,27 +4,35 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; + import { EuiCallOut } from '@elastic/eui'; +import type { FullTraceWaterfallProps } from '@kbn/apm-types'; import { i18n } from '@kbn/i18n'; -import { isPending, useFetcher } from '../../hooks/use_fetcher'; +import React from 'react'; +import type { CoreStart } from '@kbn/core/public'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { TraceWaterfall } from '.'; +import { isPending, useFetcher } from '../../../hooks/use_fetcher'; import { Loading } from './loading'; -import type { ApmTraceWaterfallEmbeddableEntryProps } from './react_embeddable_factory'; -import { TraceWaterfall } from '../../components/shared/trace_waterfall'; +import { createCallApmApi } from '../../../services/rest/create_call_apm_api'; -export function TraceWaterfallEmbeddable({ - serviceName, +interface Props extends FullTraceWaterfallProps { + core: CoreStart; +} + +export function FullTraceWaterfallRenderer({ + traceId, rangeFrom, rangeTo, - traceId, + serviceName, scrollElement, onNodeClick, - getRelatedErrorsHref, onErrorClick, - mode, -}: ApmTraceWaterfallEmbeddableEntryProps) { - const isFiltered = mode === 'filtered'; - + core, +}: Props) { + useEffectOnce(() => { + createCallApmApi(core); + }); const { data, status } = useFetcher( (callApmApi) => { return callApmApi('GET /internal/apm/unified_traces/{traceId}', { @@ -33,12 +41,11 @@ export function TraceWaterfallEmbeddable({ query: { start: rangeFrom, end: rangeTo, - serviceName: isFiltered ? serviceName : undefined, }, }, }); }, - [rangeFrom, rangeTo, traceId, isFiltered, serviceName] + [rangeFrom, rangeTo, traceId] ); if (isPending(status)) { @@ -65,12 +72,10 @@ export function TraceWaterfallEmbeddable({ errors={data.errors} onClick={onNodeClick} scrollElement={scrollElement} - getRelatedErrorsHref={getRelatedErrorsHref} isEmbeddable showLegend serviceName={serviceName} onErrorClick={onErrorClick} - isFiltered={isFiltered} agentMarks={data.agentMarks} showCriticalPathControl /> diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/trace_waterfall/lazy_create_full_trace_waterfall_renderer.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/trace_waterfall/lazy_create_full_trace_waterfall_renderer.tsx new file mode 100644 index 0000000000000..af8263d083067 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/trace_waterfall/lazy_create_full_trace_waterfall_renderer.tsx @@ -0,0 +1,22 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { dynamic } from '@kbn/shared-ux-utility'; +import type { CoreStart } from '@kbn/core/public'; +import type { FullTraceWaterfallProps } from '@kbn/apm-types'; + +const LazyFullTraceWaterfallRendererComponent = dynamic(() => + import('./full_trace_waterfall_renderer').then((mod) => ({ + default: mod.FullTraceWaterfallRenderer, + })) +); + +export function createLazyFullTraceWaterfallRenderer({ core }: { core: CoreStart }) { + return (props: FullTraceWaterfallProps) => { + return ; + }; +} diff --git a/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/loading.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/trace_waterfall/loading.tsx similarity index 100% rename from x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/loading.tsx rename to x-pack/solutions/observability/plugins/apm/public/components/shared/trace_waterfall/loading.tsx diff --git a/x-pack/solutions/observability/plugins/apm/public/embeddable/register_embeddables.tsx b/x-pack/solutions/observability/plugins/apm/public/embeddable/register_embeddables.tsx index 2823e62a321f6..60627b81c8da0 100644 --- a/x-pack/solutions/observability/plugins/apm/public/embeddable/register_embeddables.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/embeddable/register_embeddables.tsx @@ -13,7 +13,6 @@ import { APM_ALERTING_LATENCY_CHART_EMBEDDABLE, APM_ALERTING_THROUGHPUT_CHART_EMBEDDABLE, } from './alerting/constants'; -import { APM_TRACE_WATERFALL_EMBEDDABLE } from './trace_waterfall/constant'; export async function registerEmbeddables( deps: Omit @@ -50,12 +49,4 @@ export async function registerEmbeddables( pluginsStart, }); }); - - registerReactEmbeddableFactory(APM_TRACE_WATERFALL_EMBEDDABLE, async () => { - const { getApmTraceWaterfallEmbeddableFactory } = await import( - './trace_waterfall/react_embeddable_factory' - ); - - return getApmTraceWaterfallEmbeddableFactory({ ...deps, coreStart, pluginsStart }); - }); } diff --git a/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/react_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/react_embeddable_factory.tsx deleted file mode 100644 index 79464828356d4..0000000000000 --- a/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/react_embeddable_factory.tsx +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { DefaultEmbeddableApi, EmbeddableFactory } from '@kbn/embeddable-plugin/public'; -import type { SerializedTitles } from '@kbn/presentation-publishing'; -import { - initializeTitleManager, - titleComparators, - useBatchedPublishingSubjects, -} from '@kbn/presentation-publishing'; -import React from 'react'; -import { BehaviorSubject, map, merge } from 'rxjs'; -import { initializeUnsavedChanges } from '@kbn/presentation-containers'; -import { KibanaSectionErrorBoundary } from '@kbn/shared-ux-error-boundary'; -import { i18n } from '@kbn/i18n'; -import type { IWaterfallGetRelatedErrorsHref } from '../../../common/waterfall/typings'; -import { ApmEmbeddableContext } from '../embeddable_context'; -import type { EmbeddableDeps } from '../types'; -import { APM_TRACE_WATERFALL_EMBEDDABLE } from './constant'; -import { TraceWaterfallEmbeddable } from './trace_waterfall_embeddable'; -import { FocusedTraceWaterfallEmbeddable } from './focused_trace_waterfall_embeddable'; -import type { OnErrorClick } from '../../components/shared/trace_waterfall/trace_waterfall_context'; - -interface BaseProps { - traceId: string; - rangeFrom: string; - rangeTo: string; -} - -export interface ApmTraceWaterfallEmbeddableFocusedProps extends BaseProps, SerializedTitles { - docId: string; - mode: 'summary'; -} - -export interface ApmTraceWaterfallEmbeddableEntryProps extends BaseProps, SerializedTitles { - serviceName: string; - displayLimit?: number; - scrollElement?: Element; - onNodeClick?: (nodeSpanId: string) => void; - getRelatedErrorsHref?: IWaterfallGetRelatedErrorsHref; - onErrorClick?: OnErrorClick; - mode: 'full' | 'filtered'; -} - -export type ApmTraceWaterfallEmbeddableProps = - | ApmTraceWaterfallEmbeddableFocusedProps - | ApmTraceWaterfallEmbeddableEntryProps; - -export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => { - const factory: EmbeddableFactory< - ApmTraceWaterfallEmbeddableProps, - DefaultEmbeddableApi - > = { - type: APM_TRACE_WATERFALL_EMBEDDABLE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { - const state = initialState; - const titleManager = initializeTitleManager(state); - const serviceName$ = new BehaviorSubject('serviceName' in state ? state.serviceName : ''); - const traceId$ = new BehaviorSubject(state.traceId); - const rangeFrom$ = new BehaviorSubject(state.rangeFrom); - const rangeTo$ = new BehaviorSubject(state.rangeTo); - const mode$ = new BehaviorSubject(state.mode); - const displayLimit$ = new BehaviorSubject('displayLimit' in state ? state.displayLimit : 0); - const docId$ = new BehaviorSubject('docId' in state ? state.docId : ''); - const scrollElement$ = new BehaviorSubject( - 'scrollElement' in state ? state.scrollElement : undefined - ); - const onNodeClick$ = new BehaviorSubject( - 'onNodeClick' in state ? state.onNodeClick : undefined - ); - const getRelatedErrorsHref$ = new BehaviorSubject( - 'getRelatedErrorsHref' in state ? state.getRelatedErrorsHref : undefined - ); - const onErrorClick$ = new BehaviorSubject( - 'onErrorClick' in state ? state.onErrorClick : undefined - ); - - function serializeState() { - return { - ...titleManager.getLatestState(), - serviceName: serviceName$.getValue(), - traceId: traceId$.getValue(), - rangeFrom: rangeFrom$.getValue(), - rangeTo: rangeTo$.getValue(), - displayLimit: displayLimit$.getValue(), - docId: docId$.getValue(), - scrollElement: scrollElement$.getValue(), - onNodeClick: onNodeClick$.getValue(), - getRelatedErrorsHref: getRelatedErrorsHref$.getValue(), - onErrorClick: onErrorClick$.getValue(), - mode: mode$.getValue(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, - anyStateChange$: merge( - titleManager.anyStateChange$, - serviceName$, - traceId$, - rangeFrom$, - rangeTo$, - displayLimit$, - docId$, - scrollElement$, - onNodeClick$, - getRelatedErrorsHref$, - onErrorClick$, - mode$ - ).pipe(map(() => undefined)), - getComparators: () => { - return { - ...titleComparators, - serviceName: 'referenceEquality', - traceId: 'referenceEquality', - rangeFrom: 'referenceEquality', - rangeTo: 'referenceEquality', - displayLimit: 'referenceEquality', - docId: 'referenceEquality', - scrollElement: 'referenceEquality', - onNodeClick: 'referenceEquality', - getRelatedErrorsHref: 'referenceEquality', - onErrorClick: 'referenceEquality', - mode: 'referenceEquality', - }; - }, - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); - - // reset base state - traceId$.next(lastSaved?.traceId ?? ''); - rangeFrom$.next(lastSaved?.rangeFrom ?? ''); - rangeTo$.next(lastSaved?.rangeTo ?? ''); - mode$.next(lastSaved?.mode ?? 'summary'); - - // reset entry state - const entryState = lastSaved as ApmTraceWaterfallEmbeddableEntryProps; - serviceName$.next(entryState?.serviceName ?? ''); - displayLimit$.next(entryState?.displayLimit ?? 0); - scrollElement$.next(entryState?.scrollElement ?? undefined); - onNodeClick$.next(entryState?.onNodeClick ?? undefined); - getRelatedErrorsHref$.next(entryState?.getRelatedErrorsHref ?? undefined); - onErrorClick$.next(entryState?.onErrorClick ?? undefined); - - // reset focused state - const focusedState = lastSaved as ApmTraceWaterfallEmbeddableFocusedProps; - docId$.next(focusedState?.docId ?? ''); - }, - }); - - const api = finalizeApi({ - ...unsavedChangesApi, - ...titleManager.api, - serializeState, - }); - - return { - api, - Component: () => { - const [ - serviceName, - traceId, - rangeFrom, - rangeTo, - displayLimit, - docId, - scrollElement, - onNodeClick, - getRelatedErrorsHref, - onErrorClick, - mode, - ] = useBatchedPublishingSubjects( - serviceName$, - traceId$, - rangeFrom$, - rangeTo$, - displayLimit$, - docId$, - scrollElement$, - onNodeClick$, - getRelatedErrorsHref$, - onErrorClick$, - mode$ - ); - const content = - mode === 'summary' ? ( - - ) : ( - - ); - - return ( - - - {content} - - - ); - }, - }; - }, - }; - return factory; -}; diff --git a/x-pack/solutions/observability/plugins/apm/public/plugin.ts b/x-pack/solutions/observability/plugins/apm/public/plugin.ts index cd218e5770699..917842cf147d5 100644 --- a/x-pack/solutions/observability/plugins/apm/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/apm/public/plugin.ts @@ -100,6 +100,8 @@ import { featureCatalogueEntry } from './feature_catalogue_entry'; import { APMServiceDetailLocator } from './locator/service_detail_locator'; import type { ITelemetryClient } from './services/telemetry'; import { TelemetryService } from './services/telemetry'; +import { createLazyFocusedTraceWaterfallRenderer } from './components/shared/focused_trace_waterfall/lazy_create_focused_trace_waterfall_renderer'; +import { createLazyFullTraceWaterfallRenderer } from './components/shared/trace_waterfall/lazy_create_full_trace_waterfall_renderer'; export type ApmPluginSetup = ReturnType; export type ApmPluginStart = ReturnType; @@ -519,7 +521,7 @@ export class ApmPlugin implements Plugin { } public start(core: CoreStart, plugins: ApmPluginStartDeps) { - const { fleet } = plugins; + const { fleet, discoverShared } = plugins; plugins.observabilityAIAssistant?.service.register(async ({ registerRenderFunction }) => { const mod = await import('./assistant_functions'); @@ -529,6 +531,16 @@ export class ApmPlugin implements Plugin { }); }); + discoverShared.features.registry.register({ + id: 'observability-focused-trace-waterfall', + render: createLazyFocusedTraceWaterfallRenderer({ core }), + }); + + discoverShared.features.registry.register({ + id: 'observability-full-trace-waterfall', + render: createLazyFullTraceWaterfallRenderer({ core }), + }); + if (fleet) { const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData();