From 9dfbb727d0c11508f908f3d11f244abb950f4d5c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 27 Jan 2026 12:14:29 +0100 Subject: [PATCH 01/10] [Streams] Use TS instead of FROM for TSDB mode in Discover links When a stream is in time-series (TSDB) mode, the "Explore in Discover" links should use `TS` instead of `FROM` in the ES|QL query. This change: - Creates a new `useStreamTSDBMode` hook that creates a DataView and checks if the stream has TSDB characteristics - Updates 4 components to use this hook and conditionally use TS/FROM: - streams_list/index.tsx (StreamNode) - stream_chart_panel.tsx - stream_badges/index.tsx (DiscoverBadgeButton) - system_events_data.tsx The failure store redirect link is intentionally not updated as the failure store is a separate index that doesn't share TSDB characteristics. Closes #246220 --- .../public/components/stream_badges/index.tsx | 5 +- .../components/stream_chart_panel.tsx | 8 +- .../stream_systems/system_events_data.tsx | 5 +- .../public/components/streams_list/index.tsx | 8 +- .../public/hooks/use_stream_tsdb_mode.ts | 46 +++++ .../ui/fixtures/page_objects/streams_app.ts | 29 ++- .../ui/tests/tsdb_discover_links.spec.ts | 183 ++++++++++++++++++ 7 files changed, 275 insertions(+), 9 deletions(-) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_tsdb_mode.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx index e9af252c837f2..65095fadcdde4 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx @@ -22,6 +22,7 @@ import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; import { css } from '@emotion/react'; import { useKibana } from '../../hooks/use_kibana'; +import { useStreamTSDBMode } from '../../hooks/use_stream_tsdb_mode'; import { truncateText } from '../../util/truncate_text'; @@ -176,11 +177,13 @@ export function DiscoverBadgeButton({ start: { share }, }, } = useKibana(); + const { isTSDBMode } = useStreamTSDBMode(definition.stream.name); const dataStreamExists = Streams.WiredStream.GetResponse.is(definition) || definition.data_stream_exists; const indexPatterns = getIndexPatternsForStream(definition.stream); + const sourceCommand = isTSDBMode ? 'TS' : 'FROM'; const esqlQuery = indexPatterns - ? `FROM ${indexPatterns.join(', ')}${isWiredStream ? ' METADATA _source' : ''}` + ? `${sourceCommand} ${indexPatterns.join(', ')}${isWiredStream ? ' METADATA _source' : ''}` : undefined; const useUrl = share.url.locators.useUrl; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx index d73cc51551626..b0ebead85afa9 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx @@ -25,6 +25,7 @@ import { StreamsAppSearchBar } from '../../streams_app_search_bar'; import { useStreamsAppFetch } from '../../../hooks/use_streams_app_fetch'; import { useTimefilter } from '../../../hooks/use_timefilter'; import { executeEsqlQuery } from '../../../hooks/use_execute_esql_query'; +import { useStreamTSDBMode } from '../../../hooks/use_stream_tsdb_mode'; interface StreamChartPanelProps { definition: Streams.ingest.all.GetResponse; @@ -54,12 +55,15 @@ export function StreamChartPanel({ definition }: StreamChartPanelProps) { [data, timeState.asAbsoluteTimeRange] ); + const { isTSDBMode } = useStreamTSDBMode(definition.stream.name); + const queries = useMemo(() => { if (!indexPatterns) { return undefined; } - const baseQuery = `FROM ${indexPatterns.join(', ')}`; + const sourceCommand = isTSDBMode ? 'TS' : 'FROM'; + const baseQuery = `${sourceCommand} ${indexPatterns.join(', ')}`; const histogramQuery = `${baseQuery} | STATS metric = COUNT(*) BY @timestamp = BUCKET(@timestamp, ${bucketSize})`; @@ -67,7 +71,7 @@ export function StreamChartPanel({ definition }: StreamChartPanelProps) { baseQuery, histogramQuery, }; - }, [bucketSize, indexPatterns]); + }, [bucketSize, indexPatterns, isTSDBMode]); const discoverLink = useMemo(() => { if (!discoverLocator || !queries?.baseQuery) { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx index fadcadadaa253..373d081248ba6 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx @@ -14,6 +14,7 @@ import { conditionToESQL } from '@kbn/streamlang'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; import { useKibana } from '../../../hooks/use_kibana'; +import { useStreamTSDBMode } from '../../../hooks/use_stream_tsdb_mode'; import { SystemEventsSparklineLast24hrs } from './system_events_sparkline'; export const SystemEventsData = ({ @@ -28,9 +29,11 @@ export const SystemEventsData = ({ start: { share }, }, } = useKibana(); + const { isTSDBMode } = useStreamTSDBMode(definition.name); const useUrl = share.url.locators.useUrl; - const esqlQuery = `FROM ${getIndexPatternsForStream(definition).join(',')} + const sourceCommand = isTSDBMode ? 'TS' : 'FROM'; + const esqlQuery = `${sourceCommand} ${getIndexPatternsForStream(definition).join(',')} | WHERE ${conditionToESQL(system.filter)}`; const discoverLink = useUrl( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/streams_list/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/streams_list/index.tsx index 7d21af5e9fe79..6433c7c92bf15 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/streams_list/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/streams_list/index.tsx @@ -30,6 +30,7 @@ import { import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; import { NestedView } from '../nested_view'; import { useKibana } from '../../hooks/use_kibana'; +import { useStreamTSDBMode } from '../../hooks/use_stream_tsdb_mode'; export interface StreamTree { name: string; @@ -178,6 +179,8 @@ function StreamNode({ [share.url.locators] ); + const { isTSDBMode } = useStreamTSDBMode(node.name); + const discoverUrl = useMemo(() => { const indexPatterns = getIndexPatternsForStream(node.stream); @@ -185,12 +188,13 @@ function StreamNode({ return undefined; } + const sourceCommand = isTSDBMode ? 'TS' : 'FROM'; return discoverLocator.getRedirectUrl({ query: { - esql: `FROM ${indexPatterns.join(', ')}`, + esql: `${sourceCommand} ${indexPatterns.join(', ')}`, }, }); - }, [discoverLocator, node]); + }, [discoverLocator, node, isTSDBMode]); return ( { + try { + const dataView = await data.dataViews.create( + { + title: streamName, + timeFieldName: '@timestamp', + }, + undefined, + false + ); + return dataView.isTSDBMode(); + } catch (err) { + // Silently handle errors for new streams that don't have indices yet + return false; + } + }, + [data.dataViews, streamName] + ); + + return { + isTSDBMode: isTSDBMode ?? false, + loading, + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/test/scout/ui/fixtures/page_objects/streams_app.ts b/x-pack/platform/plugins/shared/streams_app/test/scout/ui/fixtures/page_objects/streams_app.ts index 480493047358e..8deedc228fbf5 100644 --- a/x-pack/platform/plugins/shared/streams_app/test/scout/ui/fixtures/page_objects/streams_app.ts +++ b/x-pack/platform/plugins/shared/streams_app/test/scout/ui/fixtures/page_objects/streams_app.ts @@ -158,7 +158,7 @@ export class StreamsApp { ).toContainText(expectedIlmPolicy); } - async verifyDiscoverButtonLink(streamName: string) { + async verifyDiscoverButtonLink(streamName: string, sourceCommand: 'FROM' | 'TS' = 'FROM') { const locator = this.page.locator( `[data-test-subj="streamsDiscoverActionButton-${streamName}"]` ); @@ -170,8 +170,8 @@ export class StreamsApp { } // Expect encoded ESQL snippet to appear (basic validation) - // 'FROM ' should appear URL-encoded - const expectedFragment = encodeURIComponent(`FROM ${streamName}`); + // ' ' should appear URL-encoded + const expectedFragment = encodeURIComponent(`${sourceCommand} ${streamName}`); if (!href.includes(expectedFragment)) { throw new Error( `Href for ${streamName} did not contain expected ESQL fragment. href=${href} expectedFragment=${expectedFragment}` @@ -179,6 +179,29 @@ export class StreamsApp { } } + async getDiscoverButtonLinkSourceCommand(streamName: string): Promise<'FROM' | 'TS' | null> { + const locator = this.page.locator( + `[data-test-subj="streamsDiscoverActionButton-${streamName}"]` + ); + await locator.waitFor(); + + const href = await locator.getAttribute('href'); + if (!href) { + return null; + } + + // Check which source command is used in the URL + const fromFragment = encodeURIComponent(`FROM ${streamName}`); + const tsFragment = encodeURIComponent(`TS ${streamName}`); + + if (href.includes(tsFragment)) { + return 'TS'; + } else if (href.includes(fromFragment)) { + return 'FROM'; + } + return null; + } + async verifyStreamsAreInTable(streamNames: string[]) { for (const name of streamNames) { await expect( diff --git a/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts b/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts new file mode 100644 index 0000000000000..68ec531b9fb28 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts @@ -0,0 +1,183 @@ +/* + * 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 { expect, type EsClient } from '@kbn/scout'; +import { test } from '../fixtures'; +import { generateLogsData } from '../fixtures/generators'; + +// TSDB-compatible stream names +const TSDB_STREAM_NAME = 'metrics-tsdb-test'; +const TSDB_TEMPLATE_NAME = 'metrics-tsdb-test-template'; + +// Regular (non-TSDB) stream for comparison +const REGULAR_STREAM_NAME = 'logs-regular-test'; + +/** + * Creates a TSDB-compatible index template with time series dimensions and metrics + */ +async function createTsdbIndexTemplate(esClient: EsClient, templateName: string, pattern: string) { + await esClient.indices.putIndexTemplate({ + name: templateName, + index_patterns: [pattern], + priority: 2000, + data_stream: {}, + template: { + settings: { + 'index.mode': 'time_series', + }, + mappings: { + properties: { + '@timestamp': { + type: 'date', + }, + 'host.name': { + type: 'keyword', + time_series_dimension: true, + }, + 'service.name': { + type: 'keyword', + time_series_dimension: true, + }, + cpu_usage: { + type: 'float', + time_series_metric: 'gauge', + }, + memory_usage: { + type: 'float', + time_series_metric: 'gauge', + }, + message: { + type: 'text', + }, + }, + }, + }, + }); +} + +/** + * Indexes sample TSDB data into the data stream + */ +async function indexTsdbData(esClient: EsClient, dataStreamName: string) { + const now = Date.now(); + const documents = []; + + // Generate 10 documents with different timestamps and dimensions + for (let i = 0; i < 10; i++) { + documents.push({ + create: {}, + }); + documents.push({ + '@timestamp': new Date(now - i * 60000).toISOString(), + 'host.name': `host-${i % 3}`, + 'service.name': `service-${i % 2}`, + cpu_usage: Math.random() * 100, + memory_usage: Math.random() * 100, + message: `Test TSDB metric ${i}`, + }); + } + + await esClient.bulk({ + index: dataStreamName, + operations: documents, + refresh: true, + }); +} + +/** + * Cleanup function to delete the TSDB template and data stream + */ +async function cleanupTsdbResources(esClient: EsClient, templateName: string, streamName: string) { + try { + await esClient.indices.deleteDataStream({ name: streamName }); + } catch (e) { + // Ignore errors if data stream doesn't exist + } + + try { + await esClient.indices.deleteIndexTemplate({ name: templateName }); + } catch (e) { + // Ignore errors if template doesn't exist + } +} + +test.describe( + 'TSDB-aware Discover links - Streams list view', + { tag: ['@ess', '@svlOblt'] }, + () => { + test.beforeAll(async ({ esClient, logsSynthtraceEsClient }) => { + // Create TSDB stream + await createTsdbIndexTemplate(esClient, TSDB_TEMPLATE_NAME, `${TSDB_STREAM_NAME}*`); + await indexTsdbData(esClient, TSDB_STREAM_NAME); + + // Create regular (non-TSDB) stream for comparison + const currentTime = Date.now(); + await generateLogsData(logsSynthtraceEsClient)({ + index: REGULAR_STREAM_NAME, + startTime: new Date(currentTime - 5 * 60 * 1000).toISOString(), + endTime: new Date(currentTime).toISOString(), + docsPerMinute: 10, + }); + }); + + test.afterAll(async ({ esClient, apiServices, logsSynthtraceEsClient }) => { + // Cleanup TSDB resources + await cleanupTsdbResources(esClient, TSDB_TEMPLATE_NAME, TSDB_STREAM_NAME); + + // Cleanup regular stream + try { + await apiServices.streams.deleteStream(REGULAR_STREAM_NAME); + } catch (e) { + // Ignore errors if stream doesn't exist + } + await logsSynthtraceEsClient.clean(); + }); + + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsAdmin(); + await pageObjects.streams.gotoStreamMainPage(); + await pageObjects.streams.expectStreamsTableVisible(); + }); + + test('should use TS source command for TSDB stream Discover link', async ({ + pageObjects, + page, + }) => { + // Poll until the TSDB mode hook resolves and the link shows 'TS' + // The hook is async so we need to wait for it to complete + await expect + .poll( + async () => { + return pageObjects.streams.getDiscoverButtonLinkSourceCommand(TSDB_STREAM_NAME); + }, + { + message: 'Expected TSDB stream Discover link to use TS source command', + timeout: 15_000, + } + ) + .toBe('TS'); + }); + + test('should use FROM source command for regular stream Discover link', async ({ + pageObjects, + }) => { + // Regular streams should already use 'FROM' without needing to wait for async resolution + // But we poll to handle any initial loading states + await expect + .poll( + async () => { + return pageObjects.streams.getDiscoverButtonLinkSourceCommand(REGULAR_STREAM_NAME); + }, + { + message: 'Expected regular stream Discover link to use FROM source command', + timeout: 10_000, + } + ) + .toBe('FROM'); + }); + } +); From 222b14bf7a4f81bd492b2b94fe418b3ff972f170 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 27 Jan 2026 13:19:11 +0100 Subject: [PATCH 02/10] [Streams] Use data_stream.index_mode from listing API for TSDB mode Instead of making N additional DataView requests to determine TSDB mode for each stream in listing views, use the index_mode field already available from the internal streams listing API response. - Add optional isTSDBMode prop to DiscoverBadgeButton - Pass TSDB mode from data_stream?.index_mode === 'time_series' in listing views - Update child_stream_list to fetch from internal API with data_stream info - Detail views continue using useStreamTSDBMode hook (single stream, acceptable) --- .../public/components/stream_badges/index.tsx | 9 ++++- .../child_stream_list.tsx | 36 +++++++++++++---- .../stream_list_view/tree_table.tsx | 1 + .../public/components/streams_list/index.tsx | 40 ++++++++++++------- 4 files changed, 62 insertions(+), 24 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx index 65095fadcdde4..b707f831d161e 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx @@ -168,16 +168,23 @@ export function LifecycleBadge({ export function DiscoverBadgeButton({ definition, isWiredStream, + isTSDBMode: isTSDBModeProp, }: { definition: Streams.ingest.all.GetResponse; isWiredStream: boolean; + /** When provided, skips the expensive hook call to determine TSDB mode */ + isTSDBMode?: boolean; }) { const { dependencies: { start: { share }, }, } = useKibana(); - const { isTSDBMode } = useStreamTSDBMode(definition.stream.name); + // Use prop if provided, otherwise fall back to hook (for detail views where listing data isn't available) + const { isTSDBMode: isTSDBModeFromHook } = useStreamTSDBMode( + isTSDBModeProp === undefined ? definition.stream.name : '' + ); + const isTSDBMode = isTSDBModeProp ?? isTSDBModeFromHook; const dataStreamExists = Streams.WiredStream.GetResponse.is(definition) || definition.data_stream_exists; const indexPatterns = getIndexPatternsForStream(definition.stream); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/child_stream_list.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/child_stream_list.tsx index 2b48e2d5df6f8..22fd7330576f6 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/child_stream_list.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/child_stream_list.tsx @@ -12,20 +12,40 @@ import { type Streams, isDescendantOf } from '@kbn/streams-schema'; import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; import { AssetImage } from '../asset_image'; -import { StreamsList } from '../streams_list'; -import { useWiredStreams } from '../../hooks/use_wired_streams'; +import { StreamsList, type StreamListItem } from '../streams_list'; +import { useKibana } from '../../hooks/use_kibana'; +import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch'; export function ChildStreamList({ definition }: { definition?: Streams.ingest.all.GetResponse }) { const router = useStreamsAppRouter(); + const { + dependencies: { + start: { + streams: { streamsRepositoryClient }, + }, + }, + } = useKibana(); - const { wiredStreams } = useWiredStreams(); + // Fetch from internal API to get data_stream info for TSDB mode detection + const { value: streamsResponse } = useStreamsAppFetch( + async ({ signal }) => + streamsRepositoryClient.fetch('GET /internal/streams', { + signal, + }), + [streamsRepositoryClient] + ); - const childrenStreams = useMemo(() => { - if (!definition) { - return []; + const childrenStreams = useMemo((): StreamListItem[] | undefined => { + if (!definition || !streamsResponse?.streams) { + return undefined; } - return wiredStreams?.filter((d) => isDescendantOf(definition.stream.name, d.name)); - }, [definition, wiredStreams]); + return streamsResponse.streams + .filter((item) => isDescendantOf(definition.stream.name, item.stream.name)) + .map((item) => ({ + stream: item.stream, + data_stream: item.data_stream, + })); + }, [definition, streamsResponse?.streams]); if (definition && childrenStreams?.length === 0) { return ( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_list_view/tree_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_list_view/tree_table.tsx index 66329e8ab9ad5..fbb9d26f76073 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_list_view/tree_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_list_view/tree_table.tsx @@ -501,6 +501,7 @@ export function StreamsTreeTable({ } as Streams.ingest.all.GetResponse } isWiredStream={item.type === 'wired'} + isTSDBMode={item.data_stream?.index_mode === 'time_series'} /> ), }, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/streams_list/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/streams_list/index.tsx index 6433c7c92bf15..9af1c48bfb33a 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/streams_list/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/streams_list/index.tsx @@ -27,41 +27,50 @@ import { isDescendantOf, isRootStreamDefinition, } from '@kbn/streams-schema'; +import type { estypes } from '@elastic/elasticsearch'; import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; import { NestedView } from '../nested_view'; import { useKibana } from '../../hooks/use_kibana'; -import { useStreamTSDBMode } from '../../hooks/use_stream_tsdb_mode'; + +export interface StreamListItem { + stream: Streams.all.Definition; + data_stream?: estypes.IndicesDataStream; +} export interface StreamTree { name: string; type: 'wired' | 'root' | 'classic'; stream: Streams.all.Definition; + data_stream?: estypes.IndicesDataStream; children: StreamTree[]; } -export function asTrees(streams: Streams.all.Definition[]) { +export function asTrees(items: StreamListItem[]) { const trees: StreamTree[] = []; - const sortedStreams = streams + const sortedItems = items .slice() - .sort((a, b) => getSegments(a.name).length - getSegments(b.name).length); + .sort((a, b) => getSegments(a.stream.name).length - getSegments(b.stream.name).length); - sortedStreams.forEach((stream) => { + sortedItems.forEach((item) => { let currentTree = trees; let existingNode: StreamTree | undefined; // traverse the tree following the prefix of the current name. // once we reach the leaf, the current name is added as child - this works because the ids are sorted by depth - while ((existingNode = currentTree.find((node) => isDescendantOf(node.name, stream.name)))) { + while ( + (existingNode = currentTree.find((node) => isDescendantOf(node.name, item.stream.name))) + ) { currentTree = existingNode.children; } if (!existingNode) { const newNode: StreamTree = { - name: stream.name, + name: item.stream.name, children: [], - stream, - type: Streams.ClassicStream.Definition.is(stream) + stream: item.stream, + data_stream: item.data_stream, + type: Streams.ClassicStream.Definition.is(item.stream) ? 'classic' - : isRootStreamDefinition(stream) + : isRootStreamDefinition(item.stream) ? 'root' : 'wired', }; @@ -77,7 +86,7 @@ export function StreamsList({ query, showControls, }: { - streams: Streams.all.Definition[] | undefined; + streams: StreamListItem[] | undefined; query?: string; showControls: boolean; }) { @@ -89,8 +98,8 @@ export function StreamsList({ const filteredItems = useMemo(() => { return items - .filter((item) => showClassic || Streams.WiredStream.Definition.is(item)) - .filter((item) => !query || item.name.toLowerCase().includes(query.toLowerCase())); + .filter((item) => showClassic || Streams.WiredStream.Definition.is(item.stream)) + .filter((item) => !query || item.stream.name.toLowerCase().includes(query.toLowerCase())); }, [query, items, showClassic]); const treeView = useMemo(() => asTrees(filteredItems), [filteredItems]); @@ -114,7 +123,7 @@ export function StreamsList({ iconType="fold" size="s" onClick={() => - setCollapsed(Object.fromEntries(items.map((item) => [item.name, true]))) + setCollapsed(Object.fromEntries(items.map((item) => [item.stream.name, true]))) } > {i18n.translate('xpack.streams.streamsTable.collapseAll', { @@ -179,7 +188,8 @@ function StreamNode({ [share.url.locators] ); - const { isTSDBMode } = useStreamTSDBMode(node.name); + // Use TSDB mode from data_stream.index_mode directly from listing data + const isTSDBMode = node.data_stream?.index_mode === 'time_series'; const discoverUrl = useMemo(() => { const indexPatterns = getIndexPatternsForStream(node.stream); From 2b74894a372fa2147fbd5df330b7d9761a8cb7ea Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 27 Jan 2026 14:28:50 +0100 Subject: [PATCH 03/10] [Streams] Use index_mode from API response instead of useStreamTSDBMode hook This removes the expensive useStreamTSDBMode hook that created DataViews to check if a stream is in TSDB mode. Instead, we now: 1. Added index_mode field to the public stream detail API response 2. Components use definition.index_mode === 'time_series' directly 3. Deleted the useStreamTSDBMode hook This improves performance by eliminating unnecessary DataView creation and ES calls when viewing stream details. --- .../shared/kbn-streams-schema/index.ts | 2 +- .../src/models/ingest/base.ts | 11 +++++ .../server/routes/streams/crud/read_stream.ts | 3 ++ .../public/components/stream_badges/index.tsx | 10 ++-- .../components/stream_chart_panel.tsx | 4 +- .../stream_systems/system_events_data.tsx | 8 +++- .../public/hooks/use_stream_tsdb_mode.ts | 46 ------------------- 7 files changed, 26 insertions(+), 58 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_tsdb_mode.ts diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts index d5c71f1a99608..e923597ad31e3 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts @@ -6,7 +6,7 @@ */ export { Streams } from './src/models/streams'; -export { IngestBase } from './src/models/ingest/base'; +export { IngestBase, type IngestStreamIndexMode } from './src/models/ingest/base'; export { Ingest } from './src/models/ingest'; export { WiredIngest } from './src/models/ingest/wired'; export { ClassicIngest } from './src/models/ingest/classic'; diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/base.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/base.ts index 03d4b1bf2bbb5..dd8fd457b455f 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/base.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/base.ts @@ -114,6 +114,8 @@ type IngestBaseStreamDefaults = { } & ModelOfSchema; /* eslint-disable @typescript-eslint/no-namespace */ +export type IngestStreamIndexMode = 'standard' | 'time_series' | 'logsdb' | 'lookup'; + export namespace IngestBaseStream { export interface Definition extends BaseStream.Definition { ingest: IngestBase; @@ -127,6 +129,7 @@ export namespace IngestBaseStream { TDefinition extends IngestBaseStream.Definition = IngestBaseStream.Definition > extends BaseStream.GetResponse { privileges: IngestStreamPrivileges; + index_mode?: IngestStreamIndexMode; } export type UpsertRequest< @@ -141,6 +144,13 @@ export namespace IngestBaseStream { } } +const ingestStreamIndexModeSchema: z.Schema = z.enum([ + 'standard', + 'time_series', + 'logsdb', + 'lookup', +]); + const IngestBaseStreamSchema = { Source: z.object({}), Definition: z.object({ @@ -148,6 +158,7 @@ const IngestBaseStreamSchema = { }), GetResponse: z.object({ privileges: ingestStreamPrivilegesSchema, + index_mode: z.optional(ingestStreamIndexModeSchema), }), UpsertRequest: z.object({}), }; diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts index 7b84463dbafb2..3e2a2eadc0eb7 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts @@ -11,6 +11,7 @@ import { getInheritedFieldsFromAncestors, getInheritedSettings, findInheritedFailureStore, + type IngestStreamIndexMode, } from '@kbn/streams-schema'; import type { IScopedClusterClient } from '@kbn/core/server'; import { isNotFoundError } from '@kbn/es-errors'; @@ -91,6 +92,7 @@ export async function readStream({ return { stream: streamDefinition, privileges, + index_mode: dataStream?.index_mode as IngestStreamIndexMode | undefined, elasticsearch_assets: dataStream && privileges.manage ? await getUnmanagedElasticsearchAssets({ @@ -130,6 +132,7 @@ export async function readStream({ rules, privileges, queries, + index_mode: dataStream?.index_mode as IngestStreamIndexMode | undefined, effective_lifecycle: findInheritedLifecycle(streamDefinition, ancestors), effective_settings: getInheritedSettings([...ancestors, streamDefinition]), inherited_fields: inheritedFields, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx index b707f831d161e..e7c55cb14776a 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx @@ -22,7 +22,6 @@ import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; import { css } from '@emotion/react'; import { useKibana } from '../../hooks/use_kibana'; -import { useStreamTSDBMode } from '../../hooks/use_stream_tsdb_mode'; import { truncateText } from '../../util/truncate_text'; @@ -172,7 +171,7 @@ export function DiscoverBadgeButton({ }: { definition: Streams.ingest.all.GetResponse; isWiredStream: boolean; - /** When provided, skips the expensive hook call to determine TSDB mode */ + /** When provided from listing data, uses this instead of definition.index_mode */ isTSDBMode?: boolean; }) { const { @@ -180,11 +179,8 @@ export function DiscoverBadgeButton({ start: { share }, }, } = useKibana(); - // Use prop if provided, otherwise fall back to hook (for detail views where listing data isn't available) - const { isTSDBMode: isTSDBModeFromHook } = useStreamTSDBMode( - isTSDBModeProp === undefined ? definition.stream.name : '' - ); - const isTSDBMode = isTSDBModeProp ?? isTSDBModeFromHook; + // Use prop if provided (from listing data), otherwise use index_mode from definition (API response) + const isTSDBMode = isTSDBModeProp ?? definition.index_mode === 'time_series'; const dataStreamExists = Streams.WiredStream.GetResponse.is(definition) || definition.data_stream_exists; const indexPatterns = getIndexPatternsForStream(definition.stream); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx index b0ebead85afa9..1c1edf375cfbd 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx @@ -25,7 +25,6 @@ import { StreamsAppSearchBar } from '../../streams_app_search_bar'; import { useStreamsAppFetch } from '../../../hooks/use_streams_app_fetch'; import { useTimefilter } from '../../../hooks/use_timefilter'; import { executeEsqlQuery } from '../../../hooks/use_execute_esql_query'; -import { useStreamTSDBMode } from '../../../hooks/use_stream_tsdb_mode'; interface StreamChartPanelProps { definition: Streams.ingest.all.GetResponse; @@ -55,7 +54,8 @@ export function StreamChartPanel({ definition }: StreamChartPanelProps) { [data, timeState.asAbsoluteTimeRange] ); - const { isTSDBMode } = useStreamTSDBMode(definition.stream.name); + // Use index_mode from API response instead of expensive DataView check + const isTSDBMode = definition.index_mode === 'time_series'; const queries = useMemo(() => { if (!indexPatterns) { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx index 373d081248ba6..f8f14e9e1114d 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx @@ -14,7 +14,7 @@ import { conditionToESQL } from '@kbn/streamlang'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; import { useKibana } from '../../../hooks/use_kibana'; -import { useStreamTSDBMode } from '../../../hooks/use_stream_tsdb_mode'; +import { useStreamDetail } from '../../../hooks/use_stream_detail'; import { SystemEventsSparklineLast24hrs } from './system_events_sparkline'; export const SystemEventsData = ({ @@ -29,7 +29,11 @@ export const SystemEventsData = ({ start: { share }, }, } = useKibana(); - const { isTSDBMode } = useStreamTSDBMode(definition.name); + // Get index_mode from the stream detail context (API response) + const { definition: fullDefinition } = useStreamDetail(); + const isTSDBMode = + Streams.ingest.all.GetResponse.is(fullDefinition) && + fullDefinition.index_mode === 'time_series'; const useUrl = share.url.locators.useUrl; const sourceCommand = isTSDBMode ? 'TS' : 'FROM'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_tsdb_mode.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_tsdb_mode.ts deleted file mode 100644 index 21d2bd2db0eb6..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_tsdb_mode.ts +++ /dev/null @@ -1,46 +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 { useAbortableAsync } from '@kbn/react-hooks'; -import { useKibana } from './use_kibana'; - -/** - * Hook to check if a stream is in TSDB (Time Series DataBase) mode. - * When in TSDB mode, ES|QL queries should use `TS` instead of `FROM`. - * - * @param streamName - The name of the stream to check - * @returns Object containing isTSDBMode boolean and loading state - */ -export function useStreamTSDBMode(streamName: string) { - const { dependencies } = useKibana(); - const { data } = dependencies.start; - - const { value: isTSDBMode, loading } = useAbortableAsync( - async ({ signal }) => { - try { - const dataView = await data.dataViews.create( - { - title: streamName, - timeFieldName: '@timestamp', - }, - undefined, - false - ); - return dataView.isTSDBMode(); - } catch (err) { - // Silently handle errors for new streams that don't have indices yet - return false; - } - }, - [data.dataViews, streamName] - ); - - return { - isTSDBMode: isTSDBMode ?? false, - loading, - }; -} From ef7ebc5b1b344c01ebc43d7ba6531f2a18ead032 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 27 Jan 2026 14:38:41 +0100 Subject: [PATCH 04/10] Update tsdb_discover_links.spec.ts --- .../test/scout/ui/tests/tsdb_discover_links.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts b/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts index 68ec531b9fb28..f7fa9426adea8 100644 --- a/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts +++ b/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts @@ -147,8 +147,6 @@ test.describe( pageObjects, page, }) => { - // Poll until the TSDB mode hook resolves and the link shows 'TS' - // The hook is async so we need to wait for it to complete await expect .poll( async () => { @@ -165,8 +163,6 @@ test.describe( test('should use FROM source command for regular stream Discover link', async ({ pageObjects, }) => { - // Regular streams should already use 'FROM' without needing to wait for async resolution - // But we poll to handle any initial loading states await expect .poll( async () => { From 3c118eb20ad2881c3615c9fdd6ca8b9d42db3ec5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 27 Jan 2026 15:06:36 +0100 Subject: [PATCH 05/10] Remove redundant isTSDBMode prop from DiscoverBadgeButton The prop was unnecessary because: - definition.index_mode is already available from the API response - In tree_table.tsx, we can include index_mode directly in the constructed definition object instead of passing a separate prop --- .../streams_app/public/components/stream_badges/index.tsx | 6 +----- .../public/components/stream_list_view/tree_table.tsx | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx index e7c55cb14776a..d8b2457c5110f 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx @@ -167,20 +167,16 @@ export function LifecycleBadge({ export function DiscoverBadgeButton({ definition, isWiredStream, - isTSDBMode: isTSDBModeProp, }: { definition: Streams.ingest.all.GetResponse; isWiredStream: boolean; - /** When provided from listing data, uses this instead of definition.index_mode */ - isTSDBMode?: boolean; }) { const { dependencies: { start: { share }, }, } = useKibana(); - // Use prop if provided (from listing data), otherwise use index_mode from definition (API response) - const isTSDBMode = isTSDBModeProp ?? definition.index_mode === 'time_series'; + const isTSDBMode = definition.index_mode === 'time_series'; const dataStreamExists = Streams.WiredStream.GetResponse.is(definition) || definition.data_stream_exists; const indexPatterns = getIndexPatternsForStream(definition.stream); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_list_view/tree_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_list_view/tree_table.tsx index fbb9d26f76073..0a088a7a3e655 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_list_view/tree_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_list_view/tree_table.tsx @@ -498,10 +498,10 @@ export function StreamsTreeTable({ { stream: item.stream, data_stream_exists: !!item.data_stream, + index_mode: item.data_stream?.index_mode, } as Streams.ingest.all.GetResponse } isWiredStream={item.type === 'wired'} - isTSDBMode={item.data_stream?.index_mode === 'time_series'} /> ), }, From 66bc64e4ba5d6a072858c248a541e8cc0fe93164 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:35:02 +0000 Subject: [PATCH 06/10] Changes from yarn openapi:bundle --- oas_docs/bundle.json | 2 +- oas_docs/bundle.serverless.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index ee1804f27fcf3..5011aa26bc04a 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -28419,7 +28419,7 @@ ], "type": "object" }, - "maxItems": 1000, + "maxItems": 10000, "type": "array" } }, diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 4152d347959b8..694ea648ec493 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -28419,7 +28419,7 @@ ], "type": "object" }, - "maxItems": 1000, + "maxItems": 10000, "type": "array" } }, From 990a70d3ead51f6b6244219fb02e6063112b502b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 27 Jan 2026 16:02:33 +0100 Subject: [PATCH 07/10] Fix TypeScript import error for Streams Changed import to use value import for Streams (needed for runtime .is() method) while keeping System as a type-only import using inline type modifier. Co-authored-by: Cursor --- .../stream_detail_systems/stream_systems/system_events_data.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx index f8f14e9e1114d..f3fade431ac7f 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { Streams, System } from '@kbn/streams-schema'; +import { Streams, type System } from '@kbn/streams-schema'; import { getIndexPatternsForStream } from '@kbn/streams-schema'; import { conditionToESQL } from '@kbn/streamlang'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; From 9addbaf9f9811b81395137a53e2d33d6ff305fd2 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 27 Jan 2026 17:13:01 +0100 Subject: [PATCH 08/10] Fix linting errors in TSDB Discover links test - Remove unused catch clause variables (use empty catch) - Move beforeEach hook before afterAll hook - Remove unused page parameter from test function Co-authored-by: Cursor --- .../ui/tests/tsdb_discover_links.spec.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts b/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts index f7fa9426adea8..5bef87b795dd8 100644 --- a/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts +++ b/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts @@ -94,13 +94,13 @@ async function indexTsdbData(esClient: EsClient, dataStreamName: string) { async function cleanupTsdbResources(esClient: EsClient, templateName: string, streamName: string) { try { await esClient.indices.deleteDataStream({ name: streamName }); - } catch (e) { + } catch { // Ignore errors if data stream doesn't exist } try { await esClient.indices.deleteIndexTemplate({ name: templateName }); - } catch (e) { + } catch { // Ignore errors if template doesn't exist } } @@ -124,6 +124,12 @@ test.describe( }); }); + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsAdmin(); + await pageObjects.streams.gotoStreamMainPage(); + await pageObjects.streams.expectStreamsTableVisible(); + }); + test.afterAll(async ({ esClient, apiServices, logsSynthtraceEsClient }) => { // Cleanup TSDB resources await cleanupTsdbResources(esClient, TSDB_TEMPLATE_NAME, TSDB_STREAM_NAME); @@ -131,22 +137,13 @@ test.describe( // Cleanup regular stream try { await apiServices.streams.deleteStream(REGULAR_STREAM_NAME); - } catch (e) { + } catch { // Ignore errors if stream doesn't exist } await logsSynthtraceEsClient.clean(); }); - test.beforeEach(async ({ browserAuth, pageObjects }) => { - await browserAuth.loginAsAdmin(); - await pageObjects.streams.gotoStreamMainPage(); - await pageObjects.streams.expectStreamsTableVisible(); - }); - - test('should use TS source command for TSDB stream Discover link', async ({ - pageObjects, - page, - }) => { + test('should use TS source command for TSDB stream Discover link', async ({ pageObjects }) => { await expect .poll( async () => { From 4dca2c95de04398403bbafc862d9db3039641ea1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 28 Jan 2026 09:17:35 +0100 Subject: [PATCH 09/10] Address review comments: type assertion, dead code, and centralize utilities - Remove unnecessary type assertion in read_stream.ts (IndicesIndexMode and IngestStreamIndexMode are structurally identical) - Delete unused useWiredStreams.ts hook (dead code) - Add isTSDBMode utility function in @kbn/streams-schema for checking time-series mode - Add getDiscoverEsqlQuery utility function to centralize ESQL query generation from stream definitions, handling FROM/TS source commands based on index mode - Update streams_list, stream_badges, system_events_data, and stream_chart_panel to use the new utilities Co-authored-by: Cursor --- .../shared/kbn-streams-schema/index.ts | 2 + .../src/helpers/get_discover_esql_query.ts | 64 +++++++++++++++++++ .../src/helpers/is_tsdb_mode.ts | 22 +++++++ .../server/routes/streams/crud/read_stream.ts | 5 +- .../public/components/stream_badges/index.tsx | 13 ++-- .../components/stream_chart_panel.tsx | 17 +++-- .../stream_systems/system_events_data.tsx | 21 +++--- .../public/components/streams_list/index.tsx | 22 ++++--- .../public/hooks/use_wired_streams.ts | 30 --------- 9 files changed, 128 insertions(+), 68 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/get_discover_esql_query.ts create mode 100644 x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/is_tsdb_mode.ts delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/use_wired_streams.ts diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts index e923597ad31e3..2411abcc4ed87 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts @@ -23,7 +23,9 @@ export { getStreamTypeFromDefinition } from './src/helpers/get_stream_type_from_ export type { StreamType } from './src/helpers/get_stream_type_from_definition'; export { isRootStreamDefinition } from './src/helpers/is_root'; export { isOtelStream } from './src/helpers/is_otel_stream'; +export { isTSDBMode } from './src/helpers/is_tsdb_mode'; export { getIndexPatternsForStream } from './src/helpers/hierarchy_helpers'; +export { getDiscoverEsqlQuery } from './src/helpers/get_discover_esql_query'; export { convertUpsertRequestIntoDefinition, convertGetResponseIntoUpsertRequest, diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/get_discover_esql_query.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/get_discover_esql_query.ts new file mode 100644 index 0000000000000..be01104d036c6 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/get_discover_esql_query.ts @@ -0,0 +1,64 @@ +/* + * 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 { IngestStreamIndexMode } from '../models/ingest/base'; +import type { Streams } from '../models/streams'; +import { getIndexPatternsForStream } from './hierarchy_helpers'; +import { isTSDBMode } from './is_tsdb_mode'; + +export interface GetDiscoverEsqlQueryOptions { + /** + * The stream definition to generate the query for + */ + definition: Streams.all.Definition; + /** + * The index mode of the stream (from API response) + */ + indexMode?: IngestStreamIndexMode; + /** + * Whether to include METADATA _source (typically for wired streams) + */ + includeMetadata?: boolean; +} + +/** + * Generates a base ES|QL query for Discover from a stream definition. + * + * Uses 'TS' source command for TSDB mode streams, 'FROM' otherwise. + * Optionally includes METADATA _source for wired streams. + * + * @param options - Configuration options for query generation + * @returns The ES|QL query string, or undefined if index patterns cannot be determined + * + * @example + * // Basic usage + * getDiscoverEsqlQuery({ definition, indexMode }) + * // Returns: "FROM logs,logs.*" + * + * @example + * // With TSDB mode + * getDiscoverEsqlQuery({ definition, indexMode: 'time_series' }) + * // Returns: "TS logs,logs.*" + * + * @example + * // With metadata for wired streams + * getDiscoverEsqlQuery({ definition, indexMode, includeMetadata: true }) + * // Returns: "FROM logs,logs.* METADATA _source" + */ +export function getDiscoverEsqlQuery(options: GetDiscoverEsqlQueryOptions): string | undefined { + const { definition, indexMode, includeMetadata = false } = options; + + const indexPatterns = getIndexPatternsForStream(definition); + if (!indexPatterns) { + return undefined; + } + + const sourceCommand = isTSDBMode(indexMode) ? 'TS' : 'FROM'; + const metadataSuffix = includeMetadata ? ' METADATA _source' : ''; + + return `${sourceCommand} ${indexPatterns.join(', ')}${metadataSuffix}`; +} diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/is_tsdb_mode.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/is_tsdb_mode.ts new file mode 100644 index 0000000000000..468ab8f913ea6 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/is_tsdb_mode.ts @@ -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 type { IngestStreamIndexMode } from '../models/ingest/base'; + +/** + * Checks if the given index mode is a TSDB (Time Series Database) mode. + * + * TSDB mode uses the 'TS' ES|QL source command instead of 'FROM' for querying. + * + * @param indexMode - The index mode to check, can be undefined + * @returns true if the index mode is 'time_series', false otherwise + */ +export function isTSDBMode( + indexMode: IngestStreamIndexMode | undefined +): indexMode is 'time_series' { + return indexMode === 'time_series'; +} diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts index 3e2a2eadc0eb7..d84fd7596e9c4 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts @@ -11,7 +11,6 @@ import { getInheritedFieldsFromAncestors, getInheritedSettings, findInheritedFailureStore, - type IngestStreamIndexMode, } from '@kbn/streams-schema'; import type { IScopedClusterClient } from '@kbn/core/server'; import { isNotFoundError } from '@kbn/es-errors'; @@ -92,7 +91,7 @@ export async function readStream({ return { stream: streamDefinition, privileges, - index_mode: dataStream?.index_mode as IngestStreamIndexMode | undefined, + index_mode: dataStream?.index_mode, elasticsearch_assets: dataStream && privileges.manage ? await getUnmanagedElasticsearchAssets({ @@ -132,7 +131,7 @@ export async function readStream({ rules, privileges, queries, - index_mode: dataStream?.index_mode as IngestStreamIndexMode | undefined, + index_mode: dataStream?.index_mode, effective_lifecycle: findInheritedLifecycle(streamDefinition, ancestors), effective_settings: getInheritedSettings([...ancestors, streamDefinition]), inherited_fields: inheritedFields, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx index d8b2457c5110f..5b2db9a0b0aae 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_badges/index.tsx @@ -15,7 +15,7 @@ import { isErrorLifecycle, isDslLifecycle, Streams, - getIndexPatternsForStream, + getDiscoverEsqlQuery, } from '@kbn/streams-schema'; import React from 'react'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; @@ -176,14 +176,13 @@ export function DiscoverBadgeButton({ start: { share }, }, } = useKibana(); - const isTSDBMode = definition.index_mode === 'time_series'; const dataStreamExists = Streams.WiredStream.GetResponse.is(definition) || definition.data_stream_exists; - const indexPatterns = getIndexPatternsForStream(definition.stream); - const sourceCommand = isTSDBMode ? 'TS' : 'FROM'; - const esqlQuery = indexPatterns - ? `${sourceCommand} ${indexPatterns.join(', ')}${isWiredStream ? ' METADATA _source' : ''}` - : undefined; + const esqlQuery = getDiscoverEsqlQuery({ + definition: definition.stream, + indexMode: definition.index_mode, + includeMetadata: isWiredStream, + }); const useUrl = share.url.locators.useUrl; const discoverLink = useUrl( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx index 1c1edf375cfbd..029ddefac50fb 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_chart_panel.tsx @@ -15,7 +15,7 @@ import { import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { Streams, getIndexPatternsForStream } from '@kbn/streams-schema'; +import { Streams, getDiscoverEsqlQuery, getIndexPatternsForStream } from '@kbn/streams-schema'; import { computeInterval } from '@kbn/visualization-utils'; import type { DurationInputArg1, DurationInputArg2 } from 'moment'; import moment from 'moment'; @@ -54,24 +54,23 @@ export function StreamChartPanel({ definition }: StreamChartPanelProps) { [data, timeState.asAbsoluteTimeRange] ); - // Use index_mode from API response instead of expensive DataView check - const isTSDBMode = definition.index_mode === 'time_series'; - const queries = useMemo(() => { - if (!indexPatterns) { + const baseQuery = getDiscoverEsqlQuery({ + definition: definition.stream, + indexMode: definition.index_mode, + }); + + if (!baseQuery) { return undefined; } - const sourceCommand = isTSDBMode ? 'TS' : 'FROM'; - const baseQuery = `${sourceCommand} ${indexPatterns.join(', ')}`; - const histogramQuery = `${baseQuery} | STATS metric = COUNT(*) BY @timestamp = BUCKET(@timestamp, ${bucketSize})`; return { baseQuery, histogramQuery, }; - }, [bucketSize, indexPatterns, isTSDBMode]); + }, [bucketSize, definition.stream, definition.index_mode]); const discoverLink = useMemo(() => { if (!discoverLocator || !queries?.baseQuery) { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx index f3fade431ac7f..af303a9241cc6 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_systems/stream_systems/system_events_data.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Streams, type System } from '@kbn/streams-schema'; -import { getIndexPatternsForStream } from '@kbn/streams-schema'; +import { Streams, type System, getDiscoverEsqlQuery } from '@kbn/streams-schema'; import { conditionToESQL } from '@kbn/streamlang'; import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; @@ -31,20 +30,22 @@ export const SystemEventsData = ({ } = useKibana(); // Get index_mode from the stream detail context (API response) const { definition: fullDefinition } = useStreamDetail(); - const isTSDBMode = - Streams.ingest.all.GetResponse.is(fullDefinition) && - fullDefinition.index_mode === 'time_series'; const useUrl = share.url.locators.useUrl; - const sourceCommand = isTSDBMode ? 'TS' : 'FROM'; - const esqlQuery = `${sourceCommand} ${getIndexPatternsForStream(definition).join(',')} - | WHERE ${conditionToESQL(system.filter)}`; + const indexMode = Streams.ingest.all.GetResponse.is(fullDefinition) + ? fullDefinition.index_mode + : undefined; + const baseQuery = getDiscoverEsqlQuery({ definition, indexMode }); + const esqlQuery = baseQuery + ? `${baseQuery} + | WHERE ${conditionToESQL(system.filter)}` + : undefined; const discoverLink = useUrl( () => ({ id: DISCOVER_APP_LOCATOR, params: { - query: { esql: esqlQuery }, + query: { esql: esqlQuery || '' }, timeRange: { from: 'now-24h', to: 'now' }, }, }), @@ -64,7 +65,7 @@ export const SystemEventsData = ({ - {discoverLink ? ( + {discoverLink && esqlQuery ? ( { - const indexPatterns = getIndexPatternsForStream(node.stream); + if (!discoverLocator) { + return undefined; + } + + // Use index_mode from data_stream (from listing data) + const esqlQuery = getDiscoverEsqlQuery({ + definition: node.stream, + indexMode: node.data_stream?.index_mode, + }); - if (!discoverLocator || !indexPatterns) { + if (!esqlQuery) { return undefined; } - const sourceCommand = isTSDBMode ? 'TS' : 'FROM'; return discoverLocator.getRedirectUrl({ query: { - esql: `${sourceCommand} ${indexPatterns.join(', ')}`, + esql: esqlQuery, }, }); - }, [discoverLocator, node, isTSDBMode]); + }, [discoverLocator, node]); return ( { - const { - dependencies: { - start: { - streams: { streamsRepositoryClient }, - }, - }, - } = useKibana(); - - const result = useStreamsAppFetch( - async ({ signal }) => streamsRepositoryClient.fetch('GET /api/streams 2023-10-31', { signal }), - [streamsRepositoryClient] - ); - - return { - wiredStreams: result.value?.streams.filter(Streams.WiredStream.Definition.is), - isLoading: result.loading, - }; -}; From 07230a1b49de1431403a21aaede494e6d03fc90d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 28 Jan 2026 09:44:14 +0100 Subject: [PATCH 10/10] Inline isTSDBMode into getDiscoverEsqlQuery - Removed the separate isTSDBMode helper function since it was only used in one place - Inlined the logic directly in getDiscoverEsqlQuery - Removed the export from kbn-streams-schema index - Deleted the is_tsdb_mode.ts helper file Co-authored-by: Cursor --- .../shared/kbn-streams-schema/index.ts | 1 - .../src/helpers/get_discover_esql_query.ts | 3 +-- .../src/helpers/is_tsdb_mode.ts | 22 ------------------- 3 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/is_tsdb_mode.ts diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts index 2411abcc4ed87..7b5a85a5da73e 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts @@ -23,7 +23,6 @@ export { getStreamTypeFromDefinition } from './src/helpers/get_stream_type_from_ export type { StreamType } from './src/helpers/get_stream_type_from_definition'; export { isRootStreamDefinition } from './src/helpers/is_root'; export { isOtelStream } from './src/helpers/is_otel_stream'; -export { isTSDBMode } from './src/helpers/is_tsdb_mode'; export { getIndexPatternsForStream } from './src/helpers/hierarchy_helpers'; export { getDiscoverEsqlQuery } from './src/helpers/get_discover_esql_query'; export { diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/get_discover_esql_query.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/get_discover_esql_query.ts index be01104d036c6..b64f5cdbd5f99 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/get_discover_esql_query.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/get_discover_esql_query.ts @@ -8,7 +8,6 @@ import type { IngestStreamIndexMode } from '../models/ingest/base'; import type { Streams } from '../models/streams'; import { getIndexPatternsForStream } from './hierarchy_helpers'; -import { isTSDBMode } from './is_tsdb_mode'; export interface GetDiscoverEsqlQueryOptions { /** @@ -57,7 +56,7 @@ export function getDiscoverEsqlQuery(options: GetDiscoverEsqlQueryOptions): stri return undefined; } - const sourceCommand = isTSDBMode(indexMode) ? 'TS' : 'FROM'; + const sourceCommand = indexMode === 'time_series' ? 'TS' : 'FROM'; const metadataSuffix = includeMetadata ? ' METADATA _source' : ''; return `${sourceCommand} ${indexPatterns.join(', ')}${metadataSuffix}`; diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/is_tsdb_mode.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/is_tsdb_mode.ts deleted file mode 100644 index 468ab8f913ea6..0000000000000 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/is_tsdb_mode.ts +++ /dev/null @@ -1,22 +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 { IngestStreamIndexMode } from '../models/ingest/base'; - -/** - * Checks if the given index mode is a TSDB (Time Series Database) mode. - * - * TSDB mode uses the 'TS' ES|QL source command instead of 'FROM' for querying. - * - * @param indexMode - The index mode to check, can be undefined - * @returns true if the index mode is 'time_series', false otherwise - */ -export function isTSDBMode( - indexMode: IngestStreamIndexMode | undefined -): indexMode is 'time_series' { - return indexMode === 'time_series'; -}