Skip to content

Commit edc360d

Browse files
[Discover][Traces] All "Open in Discover" links now open in a new Discover Tab on left-click (elastic#251103)
## Summary Closes elastic#241280 Now that we have Discover Tabs, all links from Discover within Discover should open in a new tab. This PR updates all the "Open in Discover" buttons to follow that pattern, as decided by the product team. - On left-click: it will open a new Discover tab. - On right-click: users can select to 'open new tab' in their browser. **Span/Transaction flyout** |Section|Interaction| |-|-| |Similar spans|![similar_spans_f_waterfall](https://github.com/user-attachments/assets/fa5db09d-d81a-4461-b0ff-fdb19cb4038a)| |Trace|~TODO, pending on elastic#249632 to be merged~![Screen Recording 2026-02-03 at 10 50 15](https://github.com/user-attachments/assets/a5c35ab1-c80b-4966-aa06-f00bd47a94c4)| |Errors|![errors](https://github.com/user-attachments/assets/aefed9f3-5a1b-4323-aef3-0936d75322f6)| |Logs|![logs](https://github.com/user-attachments/assets/50c36810-d409-4987-861b-aa98af8c99da)| |Span Links|![span_links](https://github.com/user-attachments/assets/9ec46de9-b5ad-4d35-9b68-0ce567e923ac)| ℹ️ Updated `data-test-subj` for Span Links and Errors sections, which were duplicated and wrong. **Log flyout** |Section|Interaction| |-|-| |Similar errors|![similar_errors](https://github.com/user-attachments/assets/d5e8dd8a-7498-436a-b502-0910ed7a5fe1)|Section only visible for logs with `trace.id`| --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent 7b9fc00 commit edc360d

24 files changed

Lines changed: 744 additions & 282 deletions

File tree

src/platform/packages/shared/kbn-unified-doc-viewer/src/services/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import type { DataView } from '@kbn/data-views-plugin/public';
11-
import type { AggregateQuery, Query } from '@kbn/es-query';
11+
import type { AggregateQuery, Query, TimeRange } from '@kbn/es-query';
1212
import type { DataTableRecord, DataTableColumnsMeta } from '@kbn/discover-utils/types';
1313
import type { RestorableStateProviderProps } from '@kbn/restorable-state';
1414
import type { ReactElement } from 'react';
@@ -43,6 +43,15 @@ export type DocViewFilterFn = (
4343
mode: '+' | '-'
4444
) => void;
4545

46+
export interface DocViewActions {
47+
openInNewTab?: (params: {
48+
query?: Query | AggregateQuery;
49+
tabLabel?: string;
50+
timeRange?: TimeRange;
51+
}) => void;
52+
updateESQLQuery?: (queryOrUpdater: string | ((prevQuery: string) => string)) => void;
53+
}
54+
4655
export interface DocViewRenderProps {
4756
hit: DataTableRecord;
4857
dataView: DataView;

src/platform/packages/shared/kbn-unified-doc-viewer/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
export type {
1111
DocView,
12+
DocViewActions,
1213
DocViewFilterFn,
1314
DocViewRenderProps,
1415
DocViewerComponent,

src/platform/packages/shared/kbn-unified-doc-viewer/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@
88
*/
99

1010
export type { ElasticRequestState } from '.';
11-
export type { DocViewFilterFn, DocViewRenderProps, DocView, DocViewerComponent } from './src/types';
11+
export type {
12+
DocViewFilterFn,
13+
DocViewRenderProps,
14+
DocView,
15+
DocViewerComponent,
16+
DocViewActions,
17+
} from './src/types';

src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/log_document_profile/accessors/get_doc_viewer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
UnifiedDocViewerLogsOverview,
1919
type UnifiedDocViewerLogsOverviewApi,
2020
} from '@kbn/unified-doc-viewer-plugin/public';
21-
import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
21+
import type { DocViewRenderProps, DocViewActions } from '@kbn/unified-doc-viewer/types';
2222
import React, { useEffect, useRef, useState } from 'react';
2323
import type { BehaviorSubject } from 'rxjs';
2424
import { filter, skip } from 'rxjs';
@@ -67,6 +67,7 @@ export const createGetDocViewer =
6767
logsAIInsightFeature={logsAIInsightFeature}
6868
streamsFeature={streamsFeature}
6969
indexes={indexes}
70+
docViewActions={params.actions}
7071
{...props}
7172
/>
7273
);
@@ -84,6 +85,7 @@ interface LogOverviewTabProps extends DocViewRenderProps {
8485
logsAIInsightFeature: ObservabilityLogsAIInsightFeature | undefined;
8586
streamsFeature: ObservabilityStreamsFeature | undefined;
8687
indexes: ObservabilityIndexes;
88+
docViewActions?: DocViewActions;
8789
}
8890

8991
const LogOverviewTab = ({
@@ -92,6 +94,7 @@ const LogOverviewTab = ({
9294
logsAIInsightFeature,
9395
streamsFeature,
9496
indexes,
97+
docViewActions,
9598
...props
9699
}: LogOverviewTabProps) => {
97100
const [logsOverviewApi, setLogsOverviewApi] = useState<UnifiedDocViewerLogsOverviewApi | null>(
@@ -102,6 +105,7 @@ const LogOverviewTab = ({
102105
return (
103106
<UnifiedDocViewerLogsOverview
104107
{...props}
108+
docViewActions={docViewActions}
105109
ref={setLogsOverviewApi}
106110
renderAIAssistant={logsAIAssistantFeature?.render}
107111
renderAIInsight={logsAIInsightFeature?.render}

src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_document_profile/document_profile/accessors/doc_viewer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ export const createGetDocViewer =
3131
title: tabTitle,
3232
order: 0,
3333
render: (props) => (
34-
<UnifiedDocViewerObservabilityGenericOverview {...props} indexes={indexes} />
34+
<UnifiedDocViewerObservabilityGenericOverview
35+
{...props}
36+
indexes={indexes}
37+
docViewActions={params.actions}
38+
/>
3539
),
3640
});
3741

src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/document_profile/accessors/doc_viewer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ export const createGetDocViewer =
3131
title: tabTitle,
3232
order: 0,
3333
render: (props) => (
34-
<UnifiedDocViewerObservabilityTracesOverview {...props} indexes={indexes} />
34+
<UnifiedDocViewerObservabilityTracesOverview
35+
{...props}
36+
indexes={indexes}
37+
docViewActions={params.actions}
38+
/>
3539
),
3640
});
3741

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

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
*/
99

1010
import React from 'react';
11-
import { render, screen, fireEvent } from '@testing-library/react';
11+
import { render, screen, fireEvent, createEvent } from '@testing-library/react';
12+
import '@testing-library/jest-dom';
1213
import userEvent from '@testing-library/user-event';
1314
import { ContentFrameworkSection, type ContentFrameworkSectionProps } from './section';
1415

@@ -71,6 +72,83 @@ describe('ContentFrameworkSection', () => {
7172
expect(defaultProps.actions?.[1].onClick).toHaveBeenCalled();
7273
});
7374

75+
it('prefers onClick over href on plain left click', () => {
76+
const onClick = jest.fn();
77+
render(
78+
<ContentFrameworkSection
79+
{...defaultProps}
80+
actions={[
81+
{
82+
icon: 'discoverApp',
83+
ariaLabel: 'Open in Discover',
84+
dataTestSubj: 'unifiedDocViewerSectionActionButton-openInDiscover',
85+
label: 'Open in Discover',
86+
href: '/app/discover',
87+
onClick,
88+
},
89+
]}
90+
/>
91+
);
92+
93+
const button = screen.getByTestId('unifiedDocViewerSectionActionButton-openInDiscover');
94+
const clickEvent = createEvent.click(button, { button: 0 });
95+
fireEvent(button, clickEvent);
96+
97+
expect(onClick).toHaveBeenCalledTimes(1);
98+
expect(clickEvent.defaultPrevented).toBe(true);
99+
});
100+
101+
it('does not intercept modifier click when href is present', () => {
102+
const onClick = jest.fn();
103+
render(
104+
<ContentFrameworkSection
105+
{...defaultProps}
106+
actions={[
107+
{
108+
icon: 'discoverApp',
109+
ariaLabel: 'Open in Discover',
110+
dataTestSubj: 'unifiedDocViewerSectionActionButton-openInDiscover',
111+
label: 'Open in Discover',
112+
href: '/app/discover',
113+
onClick,
114+
},
115+
]}
116+
/>
117+
);
118+
119+
const button = screen.getByTestId('unifiedDocViewerSectionActionButton-openInDiscover');
120+
const ctrlClickEvent = createEvent.click(button, { button: 0, ctrlKey: true });
121+
fireEvent(button, ctrlClickEvent);
122+
123+
expect(onClick).not.toHaveBeenCalled();
124+
expect(ctrlClickEvent.defaultPrevented).toBe(false);
125+
});
126+
127+
it('does not intercept middle click when href is present', () => {
128+
const onClick = jest.fn();
129+
render(
130+
<ContentFrameworkSection
131+
{...defaultProps}
132+
actions={[
133+
{
134+
icon: 'discoverApp',
135+
ariaLabel: 'Open in Discover',
136+
dataTestSubj: 'unifiedDocViewerSectionActionButton-openInDiscoverIcon',
137+
href: '/app/discover',
138+
onClick,
139+
},
140+
]}
141+
/>
142+
);
143+
144+
const button = screen.getByTestId('unifiedDocViewerSectionActionButton-openInDiscoverIcon');
145+
const middleClickEvent = createEvent.click(button, { button: 1 });
146+
fireEvent(button, middleClickEvent);
147+
148+
expect(onClick).not.toHaveBeenCalled();
149+
expect(middleClickEvent.defaultPrevented).toBe(false);
150+
});
151+
74152
it('renders children inside the panel', () => {
75153
render(<ContentFrameworkSection {...defaultProps} />);
76154
expect(screen.getByText('Section children')).toBeInTheDocument();

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ interface BaseAction {
2020
}
2121

2222
export type Action =
23-
| (BaseAction & { onClick: () => void; href?: never })
24-
| (BaseAction & { href: string; onClick?: never });
23+
| (BaseAction & { onClick: () => void; href?: string })
24+
| (BaseAction & { href: string; onClick?: () => void });
2525

2626
export interface SectionActionsProps {
2727
actions: Action[];
2828
}
2929

30+
function isPlainLeftClick(e: React.MouseEvent) {
31+
return e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey;
32+
}
33+
3034
export const SectionActions = ({ actions }: SectionActionsProps) => {
3135
if (!actions.length) return null;
3236
const size = 'xs';
@@ -35,7 +39,20 @@ export const SectionActions = ({ actions }: SectionActionsProps) => {
3539
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd" alignItems="center">
3640
{actions.map((action, idx) => {
3741
const { icon, ariaLabel, dataTestSubj, label, onClick, href } = action;
38-
const buttonProps = onClick ? { onClick } : { href };
42+
const handleClick = onClick
43+
? (e: React.MouseEvent) => {
44+
// If we have an href, keep native link behaviour for right clicks and modifier clicks.
45+
// Plain left click should run the provided handler instead.
46+
if (href && !isPlainLeftClick(e)) return;
47+
if (href) e.preventDefault();
48+
onClick();
49+
}
50+
: undefined;
51+
52+
const buttonProps = {
53+
href,
54+
onClick: handleClick,
55+
};
3956

4057
return (
4158
<EuiFlexItem grow={false} key={action.id ?? idx} id={action.id}>

src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview.tsx

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
import type { LogDocument, ObservabilityIndexes } from '@kbn/discover-utils/src';
2626
import { getStacktraceFields } from '@kbn/discover-utils/src';
2727
import { css } from '@emotion/react';
28+
import type { DocViewActions } from '@kbn/unified-doc-viewer/src/services/types';
2829
import { LogsOverviewHeader } from './logs_overview_header';
2930
import { FieldActionsProvider } from '../../hooks/use_field_actions';
3031
import { getUnifiedDocViewerServices } from '../../plugin';
@@ -39,6 +40,7 @@ import { TraceWaterfall } from '../observability/traces/components/trace_waterfa
3940
import { DataSourcesProvider } from '../../hooks/use_data_sources';
4041
import { SimilarErrors } from './sub_components/similar_errors';
4142
import { hasErrorFields } from './utils/has_error_fields';
43+
import { DocViewerExtensionActionsProvider } from '../../hooks/use_doc_viewer_extension_actions';
4244

4345
export type LogsOverviewProps = DocViewRenderProps & {
4446
renderAIAssistant?: ObservabilityLogsAIAssistantFeature['render'];
@@ -47,6 +49,7 @@ export type LogsOverviewProps = DocViewRenderProps & {
4749
renderFlyoutStreamProcessingLink?: ObservabilityStreamsFeature['renderFlyoutStreamProcessingLink'];
4850
indexes: ObservabilityIndexes;
4951
showTraceWaterfall?: boolean;
52+
docViewActions?: DocViewActions;
5053
};
5154

5255
export interface LogsOverviewApi {
@@ -69,6 +72,7 @@ export const LogsOverview = forwardRef<LogsOverviewApi, LogsOverviewProps>(
6972
renderFlyoutStreamProcessingLink,
7073
indexes,
7174
showTraceWaterfall = true,
75+
docViewActions,
7276
},
7377
ref
7478
) => {
@@ -131,24 +135,28 @@ export const LogsOverview = forwardRef<LogsOverviewApi, LogsOverviewProps>(
131135
dataView={dataView}
132136
/>
133137
<DataSourcesProvider indexes={indexes}>
134-
{showSimilarErrors ? <SimilarErrors hit={hit} /> : null}
135-
<div>{renderFlyoutStreamField && renderFlyoutStreamField({ dataView, doc: hit })}</div>
136-
<LogsOverviewDegradedFields ref={qualityIssuesSectionRef} rawDoc={hit.raw} />
137-
{isStacktraceAvailable && (
138-
<LogsOverviewStacktraceSection
139-
ref={stackTraceSectionRef}
140-
hit={hit}
141-
dataView={dataView}
142-
/>
143-
)}
144-
{traceId && showTraceWaterfall ? (
145-
<TraceWaterfall
146-
traceId={traceId}
147-
docId={parsedDoc[TRANSACTION_ID_FIELD] || parsedDoc[SPAN_ID_FIELD]}
148-
serviceName={parsedDoc[SERVICE_NAME_FIELD]}
149-
dataView={dataView}
150-
/>
151-
) : null}
138+
<DocViewerExtensionActionsProvider actions={docViewActions}>
139+
{showSimilarErrors ? <SimilarErrors hit={hit} /> : null}
140+
<div>
141+
{renderFlyoutStreamField && renderFlyoutStreamField({ dataView, doc: hit })}
142+
</div>
143+
<LogsOverviewDegradedFields ref={qualityIssuesSectionRef} rawDoc={hit.raw} />
144+
{isStacktraceAvailable && (
145+
<LogsOverviewStacktraceSection
146+
ref={stackTraceSectionRef}
147+
hit={hit}
148+
dataView={dataView}
149+
/>
150+
)}
151+
{traceId && showTraceWaterfall ? (
152+
<TraceWaterfall
153+
traceId={traceId}
154+
docId={parsedDoc[TRANSACTION_ID_FIELD] || parsedDoc[SPAN_ID_FIELD]}
155+
serviceName={parsedDoc[SERVICE_NAME_FIELD]}
156+
dataView={dataView}
157+
/>
158+
) : null}
159+
</DocViewerExtensionActionsProvider>
152160
</DataSourcesProvider>
153161
{LogsOverviewAIAssistant && <LogsOverviewAIAssistant doc={hit} />}
154162
<EuiSpacer size="m" />

0 commit comments

Comments
 (0)