Skip to content

Commit 3489cdd

Browse files
iblancofcrespocarloskibanamachine
authored
[Discover][Content Framework] Chart and SimilarSpans (elastic#233163)
## Summary Closes elastic#229630 This PR introduces the `ContentFrameworkChart`, which includes a title, a tip, a link to Open in Discover, and space to display the relevant chart. <img width="1043" height="73" alt="Screenshot 2025-08-28 at 17 22 44" src="https://github.com/user-attachments/assets/0df3676b-51e6-4d61-b625-b0ab09ad4606" /> It also introduces the `SimilarSpans` component, which uses `ContentFrameworkChart` and `ContentFrameworkSection`. This component will be used in [another issue](elastic#228916) to replace the current Duration section. <img width="593" height="355" alt="Screenshot 2025-08-27 at 18 11 17" src="https://github.com/user-attachments/assets/ee2f5806-8705-42e7-81e5-f65e379f4dc8" /> Both components are available to view in Storybook: ```` yarn storybook unified_doc_viewer ```` --------- Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent e970491 commit 3489cdd

9 files changed

Lines changed: 498 additions & 5 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { Meta, StoryObj } from '@storybook/react';
11+
import React from 'react';
12+
import type { UnifiedDocViewerStorybookArgs } from '../../../../.storybook/preview';
13+
import { ContentFrameworkChart, type ContentFrameworkChartProps } from '.';
14+
import APMSpanFixture from '../../../__fixtures__/span_apm_minimal.json';
15+
16+
type Args = UnifiedDocViewerStorybookArgs<ContentFrameworkChartProps>;
17+
const meta = {
18+
title: 'Content Framework/Chart',
19+
component: ContentFrameworkChart,
20+
} satisfies Meta<typeof ContentFrameworkChart>;
21+
22+
export default meta;
23+
type Story = StoryObj<Args>;
24+
25+
export const Basic: Story = {
26+
args: {
27+
hit: APMSpanFixture,
28+
'data-test-subj': 'id',
29+
title: 'Chart Title',
30+
description: 'This is a description for the chart.',
31+
esqlQuery: 'test',
32+
children: <div>Chart content goes here.</div>,
33+
},
34+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import React from 'react';
11+
import { render, screen } from '@testing-library/react';
12+
import type { ContentFrameworkChartProps } from '.';
13+
import { ContentFrameworkChart } from '.';
14+
15+
jest.mock('../../../plugin', () => ({
16+
getUnifiedDocViewerServices: () => ({
17+
share: {
18+
url: {
19+
locators: {
20+
get: () => ({
21+
getRedirectUrl: jest.fn(() => 'http://discover-url'),
22+
}),
23+
},
24+
},
25+
},
26+
data: {
27+
query: {
28+
timefilter: {
29+
timefilter: {
30+
getAbsoluteTime: jest.fn(() => ({ from: 'now-15m', to: 'now' })),
31+
},
32+
},
33+
},
34+
},
35+
}),
36+
}));
37+
38+
describe('ContentFrameworkChart', () => {
39+
const defaultProps: ContentFrameworkChartProps = {
40+
'data-test-subj': 'test-chart',
41+
title: 'Chart Title',
42+
description: 'Chart description',
43+
esqlQuery: 'SELECT * FROM test',
44+
children: <div>Chart content</div>,
45+
};
46+
47+
it('renders the title', () => {
48+
render(<ContentFrameworkChart {...defaultProps} />);
49+
expect(screen.getByText('Chart Title')).toBeInTheDocument();
50+
});
51+
52+
it('renders the description', async () => {
53+
render(<ContentFrameworkChart {...defaultProps} />);
54+
expect(screen.getByText('Chart description')).toBeInTheDocument();
55+
});
56+
57+
it('renders the Open in Discover button if esqlQuery and discoverUrl exist', () => {
58+
render(<ContentFrameworkChart {...defaultProps} />);
59+
const discoverBtn = screen.getByTestId('ContentFrameworkChartOpenInDiscover');
60+
expect(discoverBtn).toBeInTheDocument();
61+
expect(discoverBtn).toHaveAttribute('href', 'http://discover-url');
62+
});
63+
64+
it('does not render the Discover button if esqlQuery is missing', () => {
65+
render(<ContentFrameworkChart {...defaultProps} esqlQuery={undefined} />);
66+
expect(screen.queryByTestId('ContentFrameworkChartOpenInDiscover')).not.toBeInTheDocument();
67+
});
68+
69+
it('renders children content', () => {
70+
render(<ContentFrameworkChart {...defaultProps} />);
71+
expect(screen.getByText('Chart content')).toBeInTheDocument();
72+
});
73+
74+
it('sets the correct data-test-subj on the root element', () => {
75+
render(<ContentFrameworkChart {...defaultProps} />);
76+
expect(screen.getByTestId('test-chart')).toBeInTheDocument();
77+
});
78+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import React, { useMemo } from 'react';
11+
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui';
12+
import { i18n } from '@kbn/i18n';
13+
import { DISCOVER_APP_LOCATOR } from '@kbn/deeplinks-analytics';
14+
import { getUnifiedDocViewerServices } from '../../../plugin';
15+
16+
export interface ContentFrameworkChartProps {
17+
title: string;
18+
description?: string;
19+
esqlQuery?: string;
20+
children: React.ReactNode;
21+
'data-test-subj': string;
22+
}
23+
24+
export function ContentFrameworkChart({
25+
'data-test-subj': contentFrameworkChartDataTestSubj,
26+
title,
27+
description,
28+
esqlQuery,
29+
children,
30+
}: ContentFrameworkChartProps) {
31+
const {
32+
share: {
33+
url: { locators },
34+
},
35+
data: {
36+
query: {
37+
timefilter: { timefilter },
38+
},
39+
},
40+
} = getUnifiedDocViewerServices();
41+
const discoverLocator = useMemo(() => locators.get(DISCOVER_APP_LOCATOR), [locators]);
42+
43+
const discoverUrl = useMemo(() => {
44+
if (!discoverLocator) {
45+
return undefined;
46+
}
47+
48+
const url = discoverLocator.getRedirectUrl({
49+
timeRange: timefilter.getAbsoluteTime(),
50+
filters: [],
51+
query: {
52+
esql: esqlQuery,
53+
},
54+
});
55+
56+
return url;
57+
}, [discoverLocator, esqlQuery, timefilter]);
58+
59+
const openInDiscoverButtonLabel = i18n.translate(
60+
'unifiedDocViewer.contentFramework.chart.openInDiscover',
61+
{
62+
defaultMessage: 'Open in Discover',
63+
}
64+
);
65+
66+
return (
67+
<EuiFlexGroup
68+
direction="column"
69+
gutterSize="s"
70+
data-test-subj={contentFrameworkChartDataTestSubj}
71+
>
72+
<EuiFlexItem grow={false}>
73+
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
74+
<EuiFlexItem grow={false}>
75+
<EuiFlexGroup alignItems="center" gutterSize="xs">
76+
<EuiFlexItem grow={false}>
77+
<EuiTitle size="xxs">
78+
<h3>{title}</h3>
79+
</EuiTitle>
80+
</EuiFlexItem>
81+
{description && (
82+
<EuiFlexItem grow={false}>
83+
<EuiIconTip
84+
content={description}
85+
data-test-subj="ContentFrameworkChartDescription"
86+
size="s"
87+
color="subdued"
88+
aria-label={description}
89+
/>
90+
</EuiFlexItem>
91+
)}
92+
</EuiFlexGroup>
93+
</EuiFlexItem>
94+
{esqlQuery && discoverUrl && (
95+
<EuiFlexItem grow={false}>
96+
<EuiButtonEmpty
97+
iconType="discoverApp"
98+
href={discoverUrl}
99+
aria-label={openInDiscoverButtonLabel}
100+
data-test-subj="ContentFrameworkChartOpenInDiscover"
101+
size="xs"
102+
>
103+
{openInDiscoverButtonLabel}
104+
</EuiButtonEmpty>
105+
</EuiFlexItem>
106+
)}
107+
</EuiFlexGroup>
108+
</EuiFlexItem>
109+
<EuiFlexItem grow={true}>{children}</EuiFlexItem>
110+
</EuiFlexGroup>
111+
);
112+
}

src/platform/plugins/shared/unified_doc_viewer/public/components/content_framework/section/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ export interface ContentFrameworkSectionProps {
4141
'data-test-subj'?: string;
4242
}
4343

44-
export const ContentFrameworkSection: React.FC<ContentFrameworkSectionProps> = ({
44+
export function ContentFrameworkSection({
4545
id,
4646
title,
4747
description,
4848
actions,
4949
children,
5050
'data-test-subj': accordionDataTestSubj,
51-
}) => {
51+
}: ContentFrameworkSectionProps) {
5252
const renderActions = () => (
5353
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd" alignItems="center">
5454
{actions?.map((action, idx) => {
@@ -117,4 +117,4 @@ export const ContentFrameworkSection: React.FC<ContentFrameworkSectionProps> = (
117117
</>
118118
</EuiAccordion>
119119
);
120-
};
120+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import React from 'react';
11+
import { i18n } from '@kbn/i18n';
12+
import { DurationDistributionChart } from '@kbn/apm-ui-shared';
13+
import { ProcessorEvent } from '@kbn/apm-types-shared';
14+
import { ContentFrameworkChart } from '../../../../content_framework/chart';
15+
import { ContentFrameworkSection } from '../../../../content_framework/section';
16+
import type { SpanLatencyChartData } from '../../doc_viewer_span_overview/hooks/use_span_latency_chart';
17+
18+
export interface SimilarSpansProps {
19+
spanDuration: number;
20+
latencyChart: {
21+
data: SpanLatencyChartData | null; // TODO move this interface
22+
loading: boolean;
23+
hasError: boolean;
24+
};
25+
isOtelSpan: boolean;
26+
esqlQuery?: string;
27+
}
28+
29+
// This section will be replacing the SpanDurationSummary and TransactionDurationSummary as part of https://github.com/elastic/kibana/issues/228916
30+
export function SimilarSpans({
31+
latencyChart,
32+
spanDuration,
33+
esqlQuery,
34+
isOtelSpan,
35+
}: SimilarSpansProps) {
36+
return (
37+
<ContentFrameworkSection
38+
id="similarSpans"
39+
data-test-subj="docViewerSimilarSpansSection"
40+
title={i18n.translate('unifiedDocViewer.observability.traces.similarSpans', {
41+
defaultMessage: 'Similar spans',
42+
})}
43+
>
44+
<ContentFrameworkChart
45+
data-test-subj={`docViewerSimilarSpansLatencyChart`}
46+
title={i18n.translate('unifiedDocViewer.observability.traces.similarSpans.latency.title', {
47+
defaultMessage: 'Latency',
48+
})}
49+
esqlQuery={!latencyChart.hasError && esqlQuery ? esqlQuery : undefined}
50+
>
51+
<DurationDistributionChart
52+
data={latencyChart.data?.spanDistributionChartData ?? []}
53+
markerValue={latencyChart.data?.percentileThresholdValue ?? 0}
54+
markerCurrentEvent={spanDuration}
55+
hasData={!!latencyChart.data?.spanDistributionChartData?.length}
56+
loading={latencyChart.loading}
57+
hasError={latencyChart.hasError}
58+
eventType={ProcessorEvent.span}
59+
showAxisTitle={false}
60+
showLegend={false}
61+
isOtelData={isOtelSpan}
62+
data-test-subj="docViewerSimilarSpansDurationDistributionChart"
63+
/>
64+
</ContentFrameworkChart>
65+
</ContentFrameworkSection>
66+
);
67+
}

0 commit comments

Comments
 (0)