Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: (
Expand Down Expand Up @@ -224,7 +235,9 @@ export type ObservabilityTracesFeature =
| ObservabilityTracesFetchRootSpanByTraceIdFeature
| ObservabilityTracesFetchSpanFeature
| ObservabilityTracesFetchLatencyOverallTransactionDistributionFeature
| ObservabilityTracesFetchLatencyOverallSpanDistributionFeature;
| ObservabilityTracesFetchLatencyOverallSpanDistributionFeature
| ObservabilityFocusedTraceWaterfallFeature
| ObservabilityFullTraceWaterfallFeature;

/** ****************************************************************************************/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
data-test-subj="embeddableRenderer"
data-type={type}
data-hide-panel-chrome={hidePanelChrome}
>
Embeddable Renderer Mock
</div>
);
},
}));
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) => (
Expand Down Expand Up @@ -62,30 +41,22 @@ describe('FullScreenWaterfall', () => {
onExitFullScreen: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
capturedCallbacks = null;
beforeAll(() => {
setUnifiedDocViewerServices({
discoverShared: {
features: {
registry: {
getById: () => ({
render: () => <div data-test-subj="fullTraceWaterfall">FullTraceWaterfall</div>,
}),
},
},
},
} as unknown as UnifiedDocViewerServices);
});

it('should render APM trace waterfall embeddable with hidden chrome', () => {
render(<FullScreenWaterfall {...defaultProps} />);

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(<FullScreenWaterfall {...defaultProps} />);

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', () => {
Expand All @@ -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(<FullScreenWaterfall {...defaultProps} />);

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(<FullScreenWaterfall {...defaultProps} />);

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(<FullScreenWaterfall {...defaultProps} />);

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(<FullScreenWaterfall {...defaultProps} />);

act(() => {
capturedCallbacks.onErrorClick({
traceId: 'test-trace-id',
docId: 'test-doc-id',
errorCount: 1,
});
});
it('should display the full trace waterfall', () => {
render(<FullScreenWaterfall {...defaultProps} />);

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(<FullScreenWaterfall {...defaultProps} />);

await waitFor(() => {
expect(screen.getByTestId('embeddableRenderer')).toBeInTheDocument();
});
describe('when service name is undefined', () => {
it('does not display the full trace waterfall', () => {
render(<FullScreenWaterfall {...defaultProps} serviceName={undefined} />);

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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<string | null>(null);
const [docIndex, setDocIndex] = useState<string | undefined>(undefined);
Expand All @@ -71,64 +74,56 @@ 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)
*
*
* 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);
setDocId(null);
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 (
<EuiFlyout
session="start"
Expand All @@ -153,20 +148,16 @@ export const FullScreenWaterfall = ({
This should be removed once PresentationPanel properly supports hidePanelChrome as an out-of-the-box solution.
Comment thread
cauemarcondes marked this conversation as resolved.
Issue: https://github.com/elastic/kibana/issues/248307
*/}
<div
ref={embeddableContainerRef}
css={css`
width: 100%;
& .embPanel__content {
display: block !important;
}
`}
>
{scrollElement ? (
<EmbeddableRenderer
type="APM_TRACE_WATERFALL_EMBEDDABLE"
getParentApi={getParentApi}
hidePanelChrome
<div ref={waterfallContainerRef}>
{scrollElement && serviceName ? (
<FullTraceWaterfall
traceId={traceId}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
serviceName={serviceName}
scrollElement={scrollElement}
onNodeClick={handleNodeClick}
onErrorClick={handleErrorClick}
/>
) : null}
</div>
Expand Down
Loading
Loading