Skip to content

Commit 9494ee8

Browse files
authored
[8.x] 🌊 Streams: Link to streams in Discover (elastic#214052) (elastic#215469)
# Backport This will backport the following commits from `main` to `8.x`: - [🌊 Streams: Link to streams in Discover (elastic#214052)](elastic#214052) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Joe Reuter","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-03-19T08:56:07Z","message":"🌊 Streams: Link to streams in Discover (elastic#214052)\n\nAdding a link to the stream into the overview tab of the discover\ndocument flyout:\n\n<img width=\"228\" alt=\"Screenshot 2025-03-12 at 08 57 48\"\nsrc=\"https://github.com/user-attachments/assets/dfd396e7-b0dc-4cca-a09c-637357cc88f9\"\n/>\n\nSome reviewer notes:\n* This is using the same strategy as the observability AI assistant via\nthe discover_shared registry - streams is not an observability-only\nplugin, but for now we want to treat it like this. If we move closer to\nthis becoming a main feature, we can probably have discover depend on\nstreams directly\n* For now, it's only showing the entry in the flyout if streams is\nenabled so it's easy to test but doesn't show up accidentally. Before\nthe initial release, we can change this condition to always show for\nobservability spaces\n* Resolving an index name to a data stream needs an Elasticsearch call\nto get the index meta data. I created a new internal route for that. It\nmeans that there is a loading state in theory, but in practice it should\nresolve really quickly because it only hits the cluster state, not the\nactual data.\n* Even if no stream can be resolved it still shows the entry in the\nflyout with a `-`. This is because it avoids shifting layout and it\ndoesn't seem to hurt if it's there.\n* As I need to link to streams, I started introducing a locator - I'm\nsure it will be needed more soon. I didn't add all the possible routes\nyet, we can expand it as needed.\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>","sha":"856b2221421d5aa2a849d84880e39693e3464d6b","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport missing","Team:obs-ux-logs","backport:version","Feature:Streams","v9.1.0","v8.19.0"],"title":"🌊 Streams: Link to streams in Discover","number":214052,"url":"https://github.com/elastic/kibana/pull/214052","mergeCommit":{"message":"🌊 Streams: Link to streams in Discover (elastic#214052)\n\nAdding a link to the stream into the overview tab of the discover\ndocument flyout:\n\n<img width=\"228\" alt=\"Screenshot 2025-03-12 at 08 57 48\"\nsrc=\"https://github.com/user-attachments/assets/dfd396e7-b0dc-4cca-a09c-637357cc88f9\"\n/>\n\nSome reviewer notes:\n* This is using the same strategy as the observability AI assistant via\nthe discover_shared registry - streams is not an observability-only\nplugin, but for now we want to treat it like this. If we move closer to\nthis becoming a main feature, we can probably have discover depend on\nstreams directly\n* For now, it's only showing the entry in the flyout if streams is\nenabled so it's easy to test but doesn't show up accidentally. Before\nthe initial release, we can change this condition to always show for\nobservability spaces\n* Resolving an index name to a data stream needs an Elasticsearch call\nto get the index meta data. I created a new internal route for that. It\nmeans that there is a loading state in theory, but in practice it should\nresolve really quickly because it only hits the cluster state, not the\nactual data.\n* Even if no stream can be resolved it still shows the entry in the\nflyout with a `-`. This is because it avoids shifting layout and it\ndoesn't seem to hurt if it's there.\n* As I need to link to streams, I started introducing a locator - I'm\nsure it will be needed more soon. I didn't add all the possible routes\nyet, we can expand it as needed.\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>","sha":"856b2221421d5aa2a849d84880e39693e3464d6b"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/214052","number":214052,"mergeCommit":{"message":"🌊 Streams: Link to streams in Discover (elastic#214052)\n\nAdding a link to the stream into the overview tab of the discover\ndocument flyout:\n\n<img width=\"228\" alt=\"Screenshot 2025-03-12 at 08 57 48\"\nsrc=\"https://github.com/user-attachments/assets/dfd396e7-b0dc-4cca-a09c-637357cc88f9\"\n/>\n\nSome reviewer notes:\n* This is using the same strategy as the observability AI assistant via\nthe discover_shared registry - streams is not an observability-only\nplugin, but for now we want to treat it like this. If we move closer to\nthis becoming a main feature, we can probably have discover depend on\nstreams directly\n* For now, it's only showing the entry in the flyout if streams is\nenabled so it's easy to test but doesn't show up accidentally. Before\nthe initial release, we can change this condition to always show for\nobservability spaces\n* Resolving an index name to a data stream needs an Elasticsearch call\nto get the index meta data. I created a new internal route for that. It\nmeans that there is a loading state in theory, but in practice it should\nresolve really quickly because it only hits the cluster state, not the\nactual data.\n* Even if no stream can be resolved it still shows the entry in the\nflyout with a `-`. This is because it avoids shifting layout and it\ndoesn't seem to hurt if it's there.\n* As I need to link to streams, I started introducing a locator - I'm\nsure it will be needed more soon. I didn't add all the possible routes\nyet, we can expand it as needed.\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>","sha":"856b2221421d5aa2a849d84880e39693e3464d6b"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
1 parent 967f118 commit 9494ee8

File tree

15 files changed

+384
-5
lines changed

15 files changed

+384
-5
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export const createGetDocViewer =
2323
'observability-logs-ai-assistant'
2424
);
2525

26+
const streamsFeature = services.discoverShared.features.registry.getById('streams');
27+
2628
return {
2729
...prevDocViewer,
2830
docViewsRegistry: (registry) => {
@@ -36,6 +38,7 @@ export const createGetDocViewer =
3638
<UnifiedDocViewerLogsOverview
3739
{...props}
3840
renderAIAssistant={logsAIAssistantFeature?.render}
41+
renderStreamsField={streamsFeature?.renderStreamsField}
3942
/>
4043
),
4144
});

β€Žsrc/platform/plugins/shared/discover_shared/public/services/discover_features/types.tsβ€Ž

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ import { FeaturesRegistry } from '../../../common';
2222
* will be shown on the logs-overview preset tab of the UnifiedDocViewer.
2323
*/
2424

25+
export interface StreamsFeatureRenderDeps {
26+
doc: DataTableRecord;
27+
}
28+
29+
export interface StreamsFeature {
30+
id: 'streams';
31+
renderStreamsField: (deps: StreamsFeatureRenderDeps) => JSX.Element;
32+
}
33+
2534
export interface ObservabilityLogsAIAssistantFeatureRenderDeps {
2635
doc: DataTableRecord;
2736
}
@@ -39,7 +48,10 @@ export interface ObservabilityCreateSLOFeature {
3948
}
4049

4150
// This should be a union of all the available client features.
42-
export type DiscoverFeature = ObservabilityLogsAIAssistantFeature | ObservabilityCreateSLOFeature;
51+
export type DiscoverFeature =
52+
| StreamsFeature
53+
| ObservabilityLogsAIAssistantFeature
54+
| ObservabilityCreateSLOFeature;
4355

4456
/**
4557
* Service types

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getLogDocumentOverview } from '@kbn/discover-utils';
1313
import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
1414
import { ObservabilityLogsAIAssistantFeatureRenderDeps } from '@kbn/discover-shared-plugin/public';
1515
import { getStacktraceFields, LogDocument } from '@kbn/discover-utils/src';
16+
import { StreamsFeatureRenderDeps } from '@kbn/discover-shared-plugin/public/services/discover_features';
1617
import { LogsOverviewHeader } from './logs_overview_header';
1718
import { LogsOverviewHighlights } from './logs_overview_highlights';
1819
import { FieldActionsProvider } from '../../hooks/use_field_actions';
@@ -22,6 +23,7 @@ import { LogsOverviewStacktraceSection } from './logs_overview_stacktrace_sectio
2223

2324
export type LogsOverviewProps = DocViewRenderProps & {
2425
renderAIAssistant?: (deps: ObservabilityLogsAIAssistantFeatureRenderDeps) => JSX.Element;
26+
renderStreamsField?: (deps: StreamsFeatureRenderDeps) => JSX.Element;
2527
};
2628

2729
export function LogsOverview({
@@ -32,6 +34,7 @@ export function LogsOverview({
3234
onAddColumn,
3335
onRemoveColumn,
3436
renderAIAssistant,
37+
renderStreamsField,
3538
}: LogsOverviewProps) {
3639
const { fieldFormats } = getUnifiedDocViewerServices();
3740
const parsedDoc = getLogDocumentOverview(hit, { dataView, fieldFormats });
@@ -49,7 +52,11 @@ export function LogsOverview({
4952
<EuiSpacer size="m" />
5053
<LogsOverviewHeader doc={parsedDoc} />
5154
<EuiHorizontalRule margin="xs" />
52-
<LogsOverviewHighlights formattedDoc={parsedDoc} flattenedDoc={hit.flattened} />
55+
<LogsOverviewHighlights
56+
formattedDoc={parsedDoc}
57+
doc={hit}
58+
renderStreamsField={renderStreamsField}
59+
/>
5360
<LogsOverviewDegradedFields rawDoc={hit.raw} />
5461
{isStacktraceAvailable && <LogsOverviewStacktraceSection hit={hit} dataView={dataView} />}
5562
{LogsOverviewAIAssistant && <LogsOverviewAIAssistant doc={hit} />}

β€Žsrc/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_logs_overview/logs_overview_highlights.tsxβ€Ž

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { CloudProvider, CloudProviderIcon } from '@kbn/custom-icons';
1212
import { first } from 'lodash';
1313
import { i18n } from '@kbn/i18n';
1414
import { DataTableRecord, LogDocumentOverview, fieldConstants } from '@kbn/discover-utils';
15+
import { StreamsFeature } from '@kbn/discover-shared-plugin/public/services/discover_features';
1516
import { HighlightField } from './sub_components/highlight_field';
1617
import { HighlightSection } from './sub_components/highlight_section';
1718
import { getUnifiedDocViewerServices } from '../../plugin';
@@ -20,11 +21,14 @@ import { TraceIdHighlightField } from './sub_components/trace_id_highlight_field
2021

2122
export function LogsOverviewHighlights({
2223
formattedDoc,
23-
flattenedDoc,
24+
doc,
25+
renderStreamsField,
2426
}: {
2527
formattedDoc: LogDocumentOverview;
26-
flattenedDoc: DataTableRecord['flattened'];
28+
doc: DataTableRecord;
29+
renderStreamsField?: StreamsFeature['renderStreamsField'];
2730
}) {
31+
const flattenedDoc = doc.flattened;
2832
const {
2933
fieldsMetadata: { useFieldsMetadata },
3034
} = getUnifiedDocViewerServices();
@@ -190,6 +194,7 @@ export function LogsOverviewHighlights({
190194
{...getHighlightProps(fieldConstants.DATASTREAM_NAMESPACE_FIELD)}
191195
/>
192196
)}
197+
{renderStreamsField && renderStreamsField({ doc })}
193198
{shouldRenderHighlight(fieldConstants.AGENT_NAME_FIELD) && (
194199
<HighlightField
195200
data-test-subj="unifiedDocViewLogsOverviewLogShipper"

β€Žx-pack/platform/plugins/shared/streams/server/routes/internal/streams/crud/route.tsβ€Ž

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
9-
import { isGroupStreamDefinition } from '@kbn/streams-schema';
9+
import { StreamDefinition, isGroupStreamDefinition } from '@kbn/streams-schema';
1010
import { z } from '@kbn/zod';
1111
import { createServerRoute } from '../../../create_server_route';
1212

@@ -67,6 +67,42 @@ export const streamDetailRoute = createServerRoute({
6767
},
6868
});
6969

70+
export const resolveIndexRoute = createServerRoute({
71+
endpoint: 'GET /internal/streams/_resolve_index',
72+
options: {
73+
access: 'internal',
74+
},
75+
security: {
76+
authz: {
77+
enabled: false,
78+
reason:
79+
'This API delegates security to the currently logged in user and their Elasticsearch permissions.',
80+
},
81+
},
82+
params: z.object({
83+
query: z.object({
84+
index: z.string(),
85+
}),
86+
}),
87+
handler: async ({
88+
request,
89+
params,
90+
getScopedClients,
91+
}): Promise<{ stream?: StreamDefinition }> => {
92+
const { scopedClusterClient, streamsClient } = await getScopedClients({ request });
93+
const response = (
94+
await scopedClusterClient.asCurrentUser.indices.get({ index: params.query.index })
95+
)[params.query.index];
96+
const dataStream = response.data_stream;
97+
if (!dataStream) {
98+
return {};
99+
}
100+
const stream = await streamsClient.getStream(dataStream);
101+
return { stream };
102+
},
103+
});
104+
70105
export const internalCrudRoutes = {
71106
...streamDetailRoute,
107+
...resolveIndexRoute,
72108
};

β€Žx-pack/platform/plugins/shared/streams_app/.storybook/get_mock_streams_app_context.tsxβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-p
1616
import { fieldsMetadataPluginPublicMock } from '@kbn/fields-metadata-plugin/public/mocks';
1717
import { DataStreamsStatsClient } from '@kbn/dataset-quality-plugin/public/services/data_streams_stats/data_streams_stats_client';
1818
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
19+
import { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public';
1920
import type { StreamsAppKibanaContext } from '../public/hooks/use_kibana';
2021
import { StreamsTelemetryService } from '../public/telemetry/service';
2122

@@ -41,6 +42,7 @@ export function getMockStreamsAppContext(): StreamsAppKibanaContext {
4142
savedObjectsTagging: {} as unknown as SavedObjectTaggingPluginStart,
4243
fieldsMetadata: fieldsMetadataPluginPublicMock.createStartContract(),
4344
licensing: {} as unknown as LicensingPluginStart,
45+
discoverShared: {} as unknown as DiscoverSharedPublicStart,
4446
},
4547
},
4648
services: {

β€Žx-pack/platform/plugins/shared/streams_app/kibana.jsoncβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"streams",
1414
"data",
1515
"dataViews",
16+
"discoverShared",
1617
"unifiedSearch",
1718
"share",
1819
"savedObjectsTagging",
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; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { SerializableRecord } from '@kbn/utility-types';
9+
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
10+
11+
export const STREAMS_APP_LOCATOR = 'STREAMS_APP_LOCATOR';
12+
13+
export interface StreamsAppLocatorParams extends SerializableRecord {
14+
/**
15+
* Optionally set stream ID, if not given it will link to the listing page.
16+
*/
17+
name?: string;
18+
}
19+
20+
export type StreamsAppLocator = LocatorPublic<StreamsAppLocatorParams>;
21+
22+
export class StreamsAppLocatorDefinition implements LocatorDefinition<StreamsAppLocatorParams> {
23+
public readonly id = STREAMS_APP_LOCATOR;
24+
25+
constructor() {}
26+
27+
public readonly getLocation = async (params: StreamsAppLocatorParams) => {
28+
return {
29+
app: 'streams',
30+
path: params.name ? `/${params.name}` : '/',
31+
state: {},
32+
};
33+
};
34+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { DataTableRecord } from '@kbn/discover-utils';
9+
import { StreamsRepositoryClient } from '@kbn/streams-plugin/public/api';
10+
import { EuiFlexGroup, EuiTitle, EuiBetaBadge, EuiLoadingSpinner, EuiLink } from '@elastic/eui';
11+
import { i18n } from '@kbn/i18n';
12+
import React, { useMemo } from 'react';
13+
import { CoreStart } from '@kbn/core/public';
14+
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
15+
import { useStreamsAppFetch } from '../hooks/use_streams_app_fetch';
16+
import { StreamsAppLocator } from '../app_locator';
17+
18+
export interface DiscoverStreamsLinkProps {
19+
doc: DataTableRecord;
20+
streamsRepositoryClient: StreamsRepositoryClient;
21+
coreApplication: CoreStart['application'];
22+
locator: StreamsAppLocator;
23+
}
24+
25+
function DiscoverStreamsLink(props: DiscoverStreamsLinkProps) {
26+
return (
27+
<RedirectAppLinks coreStart={{ application: props.coreApplication }}>
28+
<EuiFlexGroup direction="column" gutterSize="xs" responsive={false}>
29+
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
30+
<EuiTitle size="xxxs">
31+
<span>
32+
{i18n.translate('xpack.streams.discoverStreamsLink.title', {
33+
defaultMessage: 'Stream',
34+
})}
35+
</span>
36+
</EuiTitle>
37+
<EuiBetaBadge
38+
size="s"
39+
label={i18n.translate('xpack.streams.betaBadgeLabel', {
40+
defaultMessage: 'Streams is currently in tech preview',
41+
})}
42+
color="hollow"
43+
iconType="beaker"
44+
/>
45+
</EuiFlexGroup>
46+
<EuiFlexGroup
47+
responsive={false}
48+
alignItems="center"
49+
justifyContent="flexStart"
50+
gutterSize="xs"
51+
>
52+
<DiscoverStreamsLinkContent {...props} />
53+
</EuiFlexGroup>
54+
</EuiFlexGroup>
55+
</RedirectAppLinks>
56+
);
57+
}
58+
59+
function getFallbackStreamName(flattenedDoc: Record<string, unknown>) {
60+
const wiredStreamName = flattenedDoc['stream.name'];
61+
if (wiredStreamName) {
62+
return String(wiredStreamName);
63+
}
64+
const dsnsType = flattenedDoc['data_stream.type'];
65+
const dsnsDataset = flattenedDoc['data_stream.dataset'];
66+
const dsnsNamespace = flattenedDoc['data_stream.namespace'];
67+
if (dsnsType && dsnsDataset && dsnsNamespace) {
68+
return `${dsnsType}-${dsnsDataset}-${dsnsNamespace}`;
69+
}
70+
return undefined;
71+
}
72+
73+
function DiscoverStreamsLinkContent({
74+
streamsRepositoryClient,
75+
doc,
76+
locator,
77+
}: DiscoverStreamsLinkProps) {
78+
const index = doc.raw._index;
79+
const flattenedDoc = doc.flattened;
80+
const { value, loading, error } = useStreamsAppFetch(
81+
async ({ signal }) => {
82+
if (!index) {
83+
return getFallbackStreamName(flattenedDoc);
84+
}
85+
const definition = await streamsRepositoryClient.fetch(
86+
'GET /internal/streams/_resolve_index',
87+
{
88+
signal,
89+
params: {
90+
query: {
91+
index,
92+
},
93+
},
94+
}
95+
);
96+
return definition?.stream?.name;
97+
},
98+
[streamsRepositoryClient, index],
99+
{ disableToastOnError: true }
100+
);
101+
const params = useMemo(() => ({ name: value }), [value]);
102+
const redirectUrl = useMemo(() => locator.getRedirectUrl(params), [locator, params]);
103+
const empty = <span>-</span>;
104+
if (!index && !value) {
105+
return empty;
106+
}
107+
if (loading) {
108+
return <EuiLoadingSpinner size="s" />;
109+
}
110+
if (error || !value) {
111+
return empty;
112+
}
113+
114+
return <EuiLink href={redirectUrl}>{value}</EuiLink>;
115+
}
116+
117+
// eslint-disable-next-line import/no-default-export
118+
export default DiscoverStreamsLink;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { StreamsFeatureRenderDeps } from '@kbn/discover-shared-plugin/public/services/discover_features';
9+
import { dynamic } from '@kbn/shared-ux-utility';
10+
import React from 'react';
11+
import { DiscoverStreamsLinkProps } from './discover_streams_link';
12+
13+
export const DiscoverStreamsLink = dynamic(() => import('./discover_streams_link'));
14+
15+
export function createDiscoverStreamsLink(services: Omit<DiscoverStreamsLinkProps, 'doc'>) {
16+
return (props: StreamsFeatureRenderDeps) => <DiscoverStreamsLink {...services} {...props} />;
17+
}

0 commit comments

Comments
Β (0)