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..7b5a85a5da73e 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'; @@ -24,6 +24,7 @@ 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 { 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..b64f5cdbd5f99 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/get_discover_esql_query.ts @@ -0,0 +1,63 @@ +/* + * 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'; + +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 = 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/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..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 @@ -91,6 +91,7 @@ export async function readStream({ return { stream: streamDefinition, privileges, + index_mode: dataStream?.index_mode, elasticsearch_assets: dataStream && privileges.manage ? await getUnmanagedElasticsearchAssets({ @@ -130,6 +131,7 @@ export async function readStream({ rules, privileges, queries, + 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 e9af252c837f2..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'; @@ -178,10 +178,11 @@ export function DiscoverBadgeButton({ } = useKibana(); const dataStreamExists = Streams.WiredStream.GetResponse.is(definition) || definition.data_stream_exists; - const indexPatterns = getIndexPatternsForStream(definition.stream); - const esqlQuery = indexPatterns - ? `FROM ${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/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_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..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'; @@ -55,19 +55,22 @@ export function StreamChartPanel({ definition }: StreamChartPanelProps) { ); const queries = useMemo(() => { - if (!indexPatterns) { + const baseQuery = getDiscoverEsqlQuery({ + definition: definition.stream, + indexMode: definition.index_mode, + }); + + if (!baseQuery) { return undefined; } - const baseQuery = `FROM ${indexPatterns.join(', ')}`; - const histogramQuery = `${baseQuery} | STATS metric = COUNT(*) BY @timestamp = BUCKET(@timestamp, ${bucketSize})`; return { baseQuery, histogramQuery, }; - }, [bucketSize, indexPatterns]); + }, [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 fadcadadaa253..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,12 +8,12 @@ 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 { 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'; import { useKibana } from '../../../hooks/use_kibana'; +import { useStreamDetail } from '../../../hooks/use_stream_detail'; import { SystemEventsSparklineLast24hrs } from './system_events_sparkline'; export const SystemEventsData = ({ @@ -28,16 +28,24 @@ export const SystemEventsData = ({ start: { share }, }, } = useKibana(); + // Get index_mode from the stream detail context (API response) + const { definition: fullDefinition } = useStreamDetail(); const useUrl = share.url.locators.useUrl; - const esqlQuery = `FROM ${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' }, }, }), @@ -57,7 +65,7 @@ export const SystemEventsData = ({ - {discoverLink ? ( + {discoverLink && esqlQuery ? ( 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', }; @@ -76,7 +86,7 @@ export function StreamsList({ query, showControls, }: { - streams: Streams.all.Definition[] | undefined; + streams: StreamListItem[] | undefined; query?: string; showControls: boolean; }) { @@ -88,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]); @@ -113,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,15 +189,23 @@ function StreamNode({ ); const discoverUrl = useMemo(() => { - 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; } return discoverLocator.getRedirectUrl({ query: { - esql: `FROM ${indexPatterns.join(', ')}`, + esql: esqlQuery, }, }); }, [discoverLocator, node]); diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_wired_streams.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_wired_streams.ts deleted file mode 100644 index d714eb0b8d3d7..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_wired_streams.ts +++ /dev/null @@ -1,30 +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 { Streams } from '@kbn/streams-schema'; -import { useKibana } from './use_kibana'; -import { useStreamsAppFetch } from './use_streams_app_fetch'; - -export const useWiredStreams = () => { - 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, - }; -}; 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..5bef87b795dd8 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/test/scout/ui/tests/tsdb_discover_links.spec.ts @@ -0,0 +1,176 @@ +/* + * 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 { + // Ignore errors if data stream doesn't exist + } + + try { + await esClient.indices.deleteIndexTemplate({ name: templateName }); + } catch { + // 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.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); + + // Cleanup regular stream + try { + await apiServices.streams.deleteStream(REGULAR_STREAM_NAME); + } catch { + // Ignore errors if stream doesn't exist + } + await logsSynthtraceEsClient.clean(); + }); + + test('should use TS source command for TSDB stream Discover link', async ({ pageObjects }) => { + 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, + }) => { + 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'); + }); + } +);