From 163c0db627c385b973b83ee866d95d11116466ec Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 6 May 2025 09:23:55 +0200 Subject: [PATCH 01/17] [Streams] Significant events view --- src/dev/storybook/aliases.ts | 1 + .../kbn-storybook/src/lib/default_config.ts | 2 +- .../src/create_traced_es_client.ts | 18 +- .../shared/kbn-streams-schema/index.ts | 14 +- .../src/api/significant_events/api.ts | 10 +- .../kbn-streams-schema/src/queries/index.ts | 10 - .../server/lib/streams/assets/asset_client.ts | 2 +- .../streams/significant_events/route.ts | 220 +++++++++- .../plugins/shared/streams/tsconfig.json | 48 +-- .../get_mock_streams_app_context.tsx | 76 +++- .../shared/streams_app/.storybook/preview.js | 7 +- .../.storybook/storybook_decorator.tsx | 7 +- .../public/components/asset_image/index.tsx | 7 + .../sig_events_empty_state_light.png | Bin 0 -> 23283 bytes .../processor_outcome_preview.tsx | 20 +- .../ingestion_rate.tsx | 22 +- .../esql_chart/controlled_esql_chart.tsx | 1 + .../public/components/spark_plot/index.tsx | 189 +++++++++ .../change_point.ts | 82 ++++ .../change_point_summary.tsx | 72 ++++ .../empty_state.stories.tsx | 35 ++ .../empty_state.tsx | 43 ++ .../index.tsx | 211 +++++++++- .../p_value_to_label.ts | 19 + .../significant_event_flyout.tsx | 384 ++++++++++++++++++ .../significant_events_flyout.stories.tsx | 35 ++ .../significant_events_histogram.tsx | 51 +++ .../significant_events_table.stories.tsx | 86 ++++ .../significant_events_table.tsx | 169 ++++++++ .../utils/discover_helpers.ts | 27 ++ ...annotation_from_formatted_change_point.tsx | 28 ++ .../components/stream_detail_view/index.tsx | 33 +- .../streams_app_search_bar/index.tsx | 3 + .../uncontrolled_streams_app_bar.tsx | 9 +- .../components/timeline/index.stories.tsx | 75 ++++ .../public/components/timeline/index.tsx | 185 +++++++++ .../hooks/use_fetch_significant_events.ts | 97 +++++ .../hooks/use_significant_events_api.ts | 99 +++++ .../public/hooks/use_stream_privileges.ts | 37 ++ .../streams_app/public/routes/config.tsx | 15 +- .../shared/streams_app/public/types.ts | 21 +- .../plugins/shared/streams_app/tsconfig.json | 79 ++-- .../packages/utils-server/tsconfig.json | 4 +- .../tsconfig.json | 2 +- 44 files changed, 2382 insertions(+), 173 deletions(-) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/asset_image/sig_events_empty_state_light.png create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/spark_plot/index.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point_summary.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.stories.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/p_value_to_label.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_flyout.stories.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_histogram.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.stories.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/discover_helpers.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/get_annotation_from_formatted_change_point.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.stories.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/use_fetch_significant_events.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/use_significant_events_api.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_privileges.ts diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index d5e78f2a69fd5..690dcc0b5a811 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -61,6 +61,7 @@ export const storybookAliases = { // security_solution_packages: 'x-pack/solutions/security/packages/storybook/config', serverless: 'src/platform/packages/shared/serverless/storybook/config', shared_ux: 'src/platform/packages/private/shared-ux/storybook/config', + streams_app: 'x-pack/platform/plugins/shared/streams_app/.storybook', threat_intelligence: 'x-pack/solutions/security/plugins/threat_intelligence/.storybook', triggers_actions_ui: 'x-pack/platform/plugins/shared/triggers_actions_ui/.storybook', ui_actions_enhanced: 'src/platform/plugins/shared/ui_actions_enhanced/.storybook', diff --git a/src/platform/packages/shared/kbn-storybook/src/lib/default_config.ts b/src/platform/packages/shared/kbn-storybook/src/lib/default_config.ts index e1a3f995ff99c..79cc2a388ecd7 100644 --- a/src/platform/packages/shared/kbn-storybook/src/lib/default_config.ts +++ b/src/platform/packages/shared/kbn-storybook/src/lib/default_config.ts @@ -20,7 +20,7 @@ import { REPO_ROOT } from './constants'; import { default as WebpackConfig } from '../webpack.config'; const MOCKS_DIRECTORY = '__storybook_mocks__'; -const EXTENSIONS = ['.ts', '.js']; +const EXTENSIONS = ['.ts', '.js', '.tsx']; /* * false is a valid option for typescript.reactDocgen, diff --git a/src/platform/packages/shared/kbn-traced-es-client/src/create_traced_es_client.ts b/src/platform/packages/shared/kbn-traced-es-client/src/create_traced_es_client.ts index 1c95770a72510..2c309467d5197 100644 --- a/src/platform/packages/shared/kbn-traced-es-client/src/create_traced_es_client.ts +++ b/src/platform/packages/shared/kbn-traced-es-client/src/create_traced_es_client.ts @@ -13,7 +13,6 @@ import type { FieldCapsResponse, MsearchRequest, ScalarValue, - SearchResponse, } from '@elastic/elasticsearch/lib/api/types'; import { withSpan } from '@kbn/apm-utils'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; @@ -85,11 +84,13 @@ export interface TracedElasticsearchClient { operationName: string, parameters: TSearchRequest ): Promise>; - msearch( + msearch( operationName: string, - parameters: MsearchRequest + parameters: TSearchRequest ): Promise<{ - responses: Array>; + responses: Array< + InferSearchResponseOf + >; }>; fieldCaps( operationName: string, @@ -200,10 +201,15 @@ export function createTracedEsClient({ >; }); }, - msearch(operationName: string, parameters: MsearchRequest) { + msearch( + operationName: string, + parameters: TSearchRequest + ) { return callWithLogger(operationName, parameters, () => { return client.msearch(parameters) as unknown as Promise<{ - responses: Array>; + responses: Array< + InferSearchResponseOf + >; }>; }); }, 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 e8bad8893393f..3d1b0f0a71fbe 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts @@ -80,7 +80,13 @@ export { export { getConditionFields } from './src/helpers/get_condition_fields'; -export { type StreamQuery, upsertStreamQueryRequestSchema, streamQuerySchema } from './src/queries'; +export { + type StreamQuery, + type StreamQueryKql, + upsertStreamQueryRequestSchema, + streamQueryKqlSchema, + streamQuerySchema, +} from './src/queries'; export { findInheritedLifecycle, findInheritingStreams } from './src/helpers/lifecycle'; @@ -121,3 +127,9 @@ export { } from './src/conditions'; export { conditionToQueryDsl } from './src/helpers/condition_to_query_dsl'; + +export type { + SignificantEventsGetResponse, + SignificantEventsPreviewResponse, + SignificantEventsResponse, +} from './src/api/significant_events/api'; diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/api.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/api.ts index a8027dd351d03..01fdaf97d1704 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/api.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/api.ts @@ -22,10 +22,16 @@ type ChangePointsType = type SignificantEventsResponse = StreamQueryKql & { occurrences: Array<{ date: string; count: number }>; change_points: { - type: Record; + type: Partial>; }; }; type SignificantEventsGetResponse = SignificantEventsResponse[]; -export type { SignificantEventsResponse, SignificantEventsGetResponse }; +type SignificantEventsPreviewResponse = SignificantEventsResponse; + +export type { + SignificantEventsResponse, + SignificantEventsGetResponse, + SignificantEventsPreviewResponse, +}; diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts index 2f23b55b9cfcb..93d7e44134459 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts @@ -24,16 +24,6 @@ export interface StreamQueryKql extends StreamQueryBase { export type StreamQuery = StreamQueryKql; -export interface StreamGetResponseBase { - dashboards: string[]; - queries: StreamQuery[]; -} - -export interface StreamUpsertRequestBase { - dashboards: string[]; - queries: StreamQuery[]; -} - const streamQueryBaseSchema: z.Schema = z.object({ id: NonEmptyString, title: NonEmptyString, diff --git a/x-pack/platform/plugins/shared/streams/server/lib/streams/assets/asset_client.ts b/x-pack/platform/plugins/shared/streams/server/lib/streams/assets/asset_client.ts index 6e9fbe14796aa..56eb757e2fe08 100644 --- a/x-pack/platform/plugins/shared/streams/server/lib/streams/assets/asset_client.ts +++ b/x-pack/platform/plugins/shared/streams/server/lib/streams/assets/asset_client.ts @@ -17,8 +17,8 @@ import { Asset, AssetLink, AssetLinkRequest, - AssetType, AssetUnlinkRequest, + AssetType, AssetWithoutUuid, DashboardLink, QueryAsset, diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts index 9af5b38483f9d..999add714eafd 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts @@ -5,17 +5,174 @@ * 2.0. */ -import { badRequest } from '@hapi/boom'; +import { + AggregationsDateHistogramAggregate, + QueryDslQueryContainer, +} from '@elastic/elasticsearch/lib/api/types'; +import { badRequest, notFound } from '@hapi/boom'; +import { ChangePointType } from '@kbn/es-types/src'; +import { + SignificantEventsGetResponse, + SignificantEventsPreviewResponse, + StreamQueryKql, + streamQueryKqlSchema, +} from '@kbn/streams-schema'; +import { createTracedEsClient } from '@kbn/traced-es-client'; import { z } from '@kbn/zod'; +import { isEmpty } from 'lodash'; import { STREAMS_API_PRIVILEGES } from '../../../../common/constants'; import { createServerRoute } from '../../create_server_route'; -import { SignificantEventsGetResponse, readSignificantEvents } from './read_significant_events'; -export const readSignificantEventsRoute = createServerRoute({ +const dateFromString = z.string().pipe(z.coerce.date()); + +function createSearchRequest({ + from, + to, + query, + bucketSize, +}: { + from: Date; + to: Date; + query: StreamQueryKql; + bucketSize: string; +}) { + return { + size: 0, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: from.toISOString(), + lte: to.toISOString(), + }, + }, + }, + { + kql: { + query: query.kql.query, + }, + } as QueryDslQueryContainer, + ], + }, + }, + aggs: { + occurrences: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + extended_bounds: { + min: from.toISOString(), + max: to.toISOString(), + }, + }, + }, + change_points: { + change_point: { + buckets_path: 'occurrences>_count', + }, + } as {}, + }, + }; +} + +const previewSignificantEventsRoute = createServerRoute({ + endpoint: 'POST /api/streams/{name}/significant_events/_preview', + params: z.object({ + path: z.object({ name: z.string() }), + query: z.object({ from: dateFromString, to: dateFromString, bucketSize: z.string() }), + body: z.object({ + query: streamQueryKqlSchema, + }), + }), + + options: { + access: 'internal', + summary: 'Read the significant events', + description: 'Read the significant events', + availability: { + stability: 'experimental', + }, + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', + }, + }, + handler: async ({ + params, + request, + getScopedClients, + logger, + }): Promise => { + const { streamsClient, scopedClusterClient } = await getScopedClients({ + request, + }); + + const isStreamEnabled = await streamsClient.isStreamsEnabled(); + if (!isStreamEnabled) { + throw badRequest('Streams is not enabled'); + } + + const tracedEsClient = createTracedEsClient({ + client: scopedClusterClient.asCurrentUser, + logger, + plugin: 'streams', + }); + + const { name } = params.path; + const { from, to, bucketSize } = params.query; + const { query } = params.body; + + const searchRequest = createSearchRequest({ + bucketSize, + from, + query, + to, + }); + + const response = await tracedEsClient.search('get_significant_event_timeseries', { + index: name, + track_total_hits: false, + ...searchRequest, + }); + + if (!response.aggregations) { + throw notFound(); + } + + const aggregations = response.aggregations as typeof response.aggregations & { + change_points: { + type: Record; + }; + }; + + return { + ...query, + change_points: aggregations.change_points, + occurrences: + aggregations.occurrences.buckets.map((bucket) => { + return { + date: bucket.key_as_string, + count: bucket.doc_count, + }; + }) ?? [], + }; + }, +}); + +const readSignificantEventsRoute = createServerRoute({ endpoint: 'GET /api/streams/{name}/significant_events 2023-10-31', params: z.object({ path: z.object({ name: z.string() }), - query: z.object({ from: z.coerce.date(), to: z.coerce.date(), bucketSize: z.string() }), + query: z.object({ + from: dateFromString, + to: dateFromString, + bucketSize: z.string(), + }), }), options: { @@ -44,13 +201,60 @@ export const readSignificantEventsRoute = createServerRoute({ const { name } = params.path; const { from, to, bucketSize } = params.query; - return await readSignificantEvents( - { name, from, to, bucketSize }, - { assetClient, scopedClusterClient } - ); + const assetQueries = await assetClient.getAssetLinks(name, ['query']); + if (isEmpty(assetQueries)) { + return []; + } + + const searchRequests = assetQueries.flatMap((asset) => { + return [ + { index: name }, + createSearchRequest({ + from, + to, + bucketSize, + query: asset.query, + }), + ]; + }); + + const response = await scopedClusterClient.asCurrentUser.msearch< + unknown, + { occurrences: AggregationsDateHistogramAggregate; change_points: unknown } + >({ searches: searchRequests }); + + const significantEvents = response.responses.map((queryResponse, queryIndex) => { + const query = assetQueries[queryIndex]; + if ('error' in queryResponse) { + return { + id: query.query.id, + title: query.query.title, + kql: query.query.kql, + occurrences: [], + change_points: {}, + }; + } + + return { + id: query.query.id, + title: query.query.title, + kql: query.query.kql, + occurrences: + // @ts-ignore map unrecognized on buckets + queryResponse?.aggregations?.occurrences?.buckets.map((bucket) => ({ + date: bucket.key_as_string, + count: bucket.doc_count, + })), + change_points: queryResponse?.aggregations?.change_points, + }; + }); + + // @ts-ignore + return significantEvents; }, }); export const significantEventsRoutes = { ...readSignificantEventsRoute, + ...previewSignificantEventsRoute, }; diff --git a/x-pack/platform/plugins/shared/streams/tsconfig.json b/x-pack/platform/plugins/shared/streams/tsconfig.json index 8f770d3c202b7..e858b0f81a8b5 100644 --- a/x-pack/platform/plugins/shared/streams/tsconfig.json +++ b/x-pack/platform/plugins/shared/streams/tsconfig.json @@ -14,39 +14,39 @@ "target/**/*" ], "kbn_references": [ + "@kbn/streams-schema", "@kbn/config-schema", "@kbn/core", - "@kbn/logging", - "@kbn/core-plugins-server", - "@kbn/core-http-server", - "@kbn/security-plugin", - "@kbn/core-saved-objects-api-server", - "@kbn/core-elasticsearch-server", - "@kbn/task-manager-plugin", "@kbn/server-route-repository", - "@kbn/zod", - "@kbn/encrypted-saved-objects-plugin", - "@kbn/licensing-plugin", "@kbn/server-route-repository-client", - "@kbn/object-utils", + "@kbn/cloud-plugin", + "@kbn/logging", + "@kbn/content-packs-schema", + "@kbn/core-plugins-server", "@kbn/alerting-plugin", - "@kbn/std", - "@kbn/safer-lodash-set", - "@kbn/streams-schema", + "@kbn/storage-adapter", "@kbn/es-errors", + "@kbn/core-elasticsearch-server", + "@kbn/safer-lodash-set", + "@kbn/core-analytics-server", + "@kbn/features-plugin", + "@kbn/i18n", + "@kbn/zod", + "@kbn/utils", + "@kbn/fleet-plugin", "@kbn/server-route-repository-utils", - "@kbn/inference-plugin", - "@kbn/storage-adapter", - "@kbn/traced-es-client", "@kbn/es-query", + "@kbn/traced-es-client", + "@kbn/object-utils", + "@kbn/inference-plugin", "@kbn/core-elasticsearch-client-server-internal", - "@kbn/utils", - "@kbn/core-analytics-server", - "@kbn/fleet-plugin", - "@kbn/content-packs-schema", - "@kbn/cloud-plugin", + "@kbn/std", "@kbn/es-types", - "@kbn/features-plugin", - "@kbn/i18n" + "@kbn/core-http-server", + "@kbn/core-saved-objects-api-server", + "@kbn/security-plugin", + "@kbn/encrypted-saved-objects-plugin", + "@kbn/licensing-plugin", + "@kbn/task-manager-plugin", ] } diff --git a/x-pack/platform/plugins/shared/streams_app/.storybook/get_mock_streams_app_context.tsx b/x-pack/platform/plugins/shared/streams_app/.storybook/get_mock_streams_app_context.tsx index 31bafa9cb2144..7724cfe4a142b 100644 --- a/x-pack/platform/plugins/shared/streams_app/.storybook/get_mock_streams_app_context.tsx +++ b/x-pack/platform/plugins/shared/streams_app/.storybook/get_mock_streams_app_context.tsx @@ -5,27 +5,27 @@ * 2.0. */ -import { ChartsPluginStart } from '@kbn/charts-plugin/public'; -import { Subject } from 'rxjs'; +import { getChartsTheme } from '@elastic/charts'; import { coreMock } from '@kbn/core/public/mocks'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { DataStreamsStatsClient } from '@kbn/dataset-quality-plugin/public/services/data_streams_stats/data_streams_stats_client'; -import type { DiscoverStart } from '@kbn/discover-plugin/public'; +import { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import { fieldsMetadataPluginPublicMock } from '@kbn/fields-metadata-plugin/public/mocks'; import { IndexManagementPluginStart } from '@kbn/index-management-shared-types'; import { IngestPipelinesPluginStart } from '@kbn/ingest-pipelines-plugin/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; -import { NavigationPublicStart } from '@kbn/navigation-plugin/public/types'; -import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; -import type { SharePublicStart } from '@kbn/share-plugin/public/plugin'; -import type { StreamsPluginStart } from '@kbn/streams-plugin/public'; -import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; +import { IUnifiedSearchPluginServices, SearchBar } from '@kbn/unified-search-plugin/public'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { merge } from 'lodash'; +import React, { useMemo } from 'react'; +import { Subject } from 'rxjs'; +import { DeepPartial } from 'utility-types'; import type { StreamsAppKibanaContext } from '../public/hooks/use_kibana'; import { StreamsTelemetryService } from '../public/telemetry/service'; +import { StreamsAppStartDependencies } from '../public/types'; export function getMockStreamsAppContext(): StreamsAppKibanaContext { const appParams = coreMock.createAppMountParameters(); @@ -63,23 +63,55 @@ export function getMockStreamsAppContext(): StreamsAppKibanaContext { core, dependencies: { start: { - dataViews: {} as unknown as DataViewsPublicPluginStart, - data: {} as unknown as DataPublicPluginStart, - unifiedSearch: {} as unknown as UnifiedSearchPublicPluginStart, - streams: {} as unknown as StreamsPluginStart, - share: {} as unknown as SharePublicStart, - navigation: {} as unknown as NavigationPublicStart, - savedObjectsTagging: {} as unknown as SavedObjectTaggingPluginStart, + dataViews: {}, + data: dataMock, + unifiedSearch: merge({}, unifiedSearchPluginMock.createStartContract(), { + ui: { + SearchBar: function SearchBarWithContext(props: {}) { + const unifiedSearchServices = useMemo(() => { + return { + data: dataMock, + storage: new Storage(window.localStorage), + uiSettings: core.uiSettings, + } as unknown as IUnifiedSearchPluginServices; + }, []); + return ( + + + + ); + }, + }, + }), + streams: {}, + share: {}, + navigation: {}, + savedObjectsTagging: {}, fieldsMetadata: fieldsMetadataPluginPublicMock.createStartContract(), licensing: {} as unknown as LicensingPluginStart, indexManagement: {} as unknown as IndexManagementPluginStart, ingestPipelines: {} as unknown as IngestPipelinesPluginStart, discoverShared: {} as unknown as DiscoverSharedPublicStart, - charts: {} as unknown as ChartsPluginStart, - discover: {} as unknown as DiscoverStart, + discover: { + locator: { + getRedirectUrl: () => '', + }, + }, observabilityAIAssistant: {} as unknown as ObservabilityAIAssistantPublicStart, - }, - }, + charts: { + theme: { + useSparklineOverrides: () => { + return {} as ReturnType< + StreamsAppStartDependencies['charts']['theme']['useSparklineOverrides'] + >; + }, + useChartsBaseTheme: () => { + return getChartsTheme({ name: 'base', darkMode: false }); + }, + }, + }, + } as DeepPartial, + } as { start: StreamsAppStartDependencies }, services: { dataStreamsClient: Promise.resolve({} as unknown as DataStreamsStatsClient), PageTemplate: () => null, diff --git a/x-pack/platform/plugins/shared/streams_app/.storybook/preview.js b/x-pack/platform/plugins/shared/streams_app/.storybook/preview.js index c8155e9c3d92c..17241476a9ad8 100644 --- a/x-pack/platform/plugins/shared/streams_app/.storybook/preview.js +++ b/x-pack/platform/plugins/shared/streams_app/.storybook/preview.js @@ -5,9 +5,6 @@ * 2.0. */ -import { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common'; -import * as jest from 'jest-mock'; +import { StreamsAppStorybookDecorator } from './storybook_decorator'; -window.jest = jest; - -export const decorators = [EuiThemeProviderDecorator]; +export const decorators = [StreamsAppStorybookDecorator]; diff --git a/x-pack/platform/plugins/shared/streams_app/.storybook/storybook_decorator.tsx b/x-pack/platform/plugins/shared/streams_app/.storybook/storybook_decorator.tsx index 617b5aee8128f..2a8a2429ceb79 100644 --- a/x-pack/platform/plugins/shared/streams_app/.storybook/storybook_decorator.tsx +++ b/x-pack/platform/plugins/shared/streams_app/.storybook/storybook_decorator.tsx @@ -5,14 +5,17 @@ * 2.0. */ import React, { ComponentType, useMemo } from 'react'; +import { EuiThemeProvider } from '@elastic/eui'; import { StreamsAppContextProvider } from '../public/components/streams_app_context_provider'; import { getMockStreamsAppContext } from './get_mock_streams_app_context'; -export function KibanaReactStorybookDecorator(Story: ComponentType) { +export function StreamsAppStorybookDecorator(Story: ComponentType) { const context = useMemo(() => getMockStreamsAppContext(), []); return ( - + + + ); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/index.tsx index 93887b1be3eee..aa13d649afe12 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/index.tsx @@ -25,6 +25,13 @@ const imageSets = { defaultMessage: 'No results image for the streams app', }), }, + significantEventsEmptyState: { + light: () => import('./sig_events_empty_state_light.png'), + dark: () => import('./sig_events_empty_state_light.png'), + alt: i18n.translate('xpack.streams.significantEvents.emptyStateImage', { + defaultMessage: 'Empty state illustration for the Significant events view', + }), + }, }; interface AssetImageProps extends Omit { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/sig_events_empty_state_light.png b/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/sig_events_empty_state_light.png new file mode 100644 index 0000000000000000000000000000000000000000..5ca27cb0cc803eda787168d210a4bc94c2e6cfbd GIT binary patch literal 23283 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 { const isLoading = useSimulatorSelector( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/ingestion_rate.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/ingestion_rate.tsx index bc1b56ccbb158..ec7c94b4664c8 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/ingestion_rate.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/ingestion_rate.tsx @@ -5,29 +5,29 @@ * 2.0. */ -import moment from 'moment'; -import React from 'react'; -import { capitalize } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { PhaseName, Streams, isIlmLifecycle } from '@kbn/streams-schema'; +import { AreaSeries, Axis, BarSeries, Chart, Settings } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem, + EuiIconTip, EuiLoadingChart, EuiPanel, EuiSpacer, - EuiIconTip, EuiText, } from '@elastic/eui'; -import { AreaSeries, Axis, BarSeries, Chart, Settings } from '@elastic/charts'; import { useElasticChartsTheme } from '@kbn/charts-theme'; import { TimeState } from '@kbn/es-query'; -import { DataStreamStats } from './hooks/use_data_stream_stats'; -import { formatBytes } from './helpers/format_bytes'; +import { i18n } from '@kbn/i18n'; +import { PhaseName, Streams, isIlmLifecycle } from '@kbn/streams-schema'; +import { capitalize } from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import { useTimefilter } from '../../../hooks/use_timefilter'; import { StreamsAppSearchBar } from '../../streams_app_search_bar'; -import { useIngestionRate, useIngestionRatePerTier } from './hooks/use_ingestion_rate'; +import { formatBytes } from './helpers/format_bytes'; +import { DataStreamStats } from './hooks/use_data_stream_stats'; import { useIlmPhasesColorAndDescription } from './hooks/use_ilm_phases_color_and_description'; -import { useTimefilter } from '../../../hooks/use_timefilter'; +import { useIngestionRate, useIngestionRatePerTier } from './hooks/use_ingestion_rate'; export function IngestionRate({ definition, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/esql_chart/controlled_esql_chart.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/esql_chart/controlled_esql_chart.tsx index b697df999acd9..e37ede673b55e 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/esql_chart/controlled_esql_chart.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/esql_chart/controlled_esql_chart.tsx @@ -189,6 +189,7 @@ export function ControlledEsqlChart({ yAccessors={serie.metricNames} data={serie.data} curve={CurveType.CURVE_MONOTONE_X} + enableHistogramMode /> ); })} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/spark_plot/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/spark_plot/index.tsx new file mode 100644 index 0000000000000..5daefcb1deb45 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/spark_plot/index.tsx @@ -0,0 +1,189 @@ +/* + * 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 { + AnnotationDomainType, + Axis, + BarSeries, + Chart, + CurveType, + LineAnnotation, + LineSeries, + PartialTheme, + Position, + ScaleType, + Settings, + TickFormatter, + Tooltip, + niceTimeFormatter, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiPanel, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; +import { useKibana } from '../../hooks/use_kibana'; + +function AnnotationTooltip({ + timestamp, + label, + xFormatter, +}: { + timestamp: number; + label: React.ReactNode; + xFormatter: TickFormatter; +}) { + const formattedTime = xFormatter(timestamp); + + return ( + + + {formattedTime} + {label} + + + ); +} + +export interface SparkPlotAnnotation { + id: string; + x: number; + color: string; + icon: React.ReactNode; + label: React.ReactNode; +} + +export function SparkPlot({ + id, + name, + type, + timeseries, + annotations, + compressed, + xFormatter: givenXFormatter, +}: { + id: string; + name?: string; + type: 'line' | 'bar'; + timeseries: Array<{ x: number; y: number | null }>; + annotations?: SparkPlotAnnotation[]; + compressed?: boolean; + xFormatter?: TickFormatter; +}) { + const { + dependencies: { + start: { charts }, + }, + } = useKibana(); + + const baseTheme = charts.theme.useChartsBaseTheme(); + + const defaultTheme = charts.theme.chartsDefaultBaseTheme; + + const sparkplotChartTheme: PartialTheme = { + chartMargins: { left: 0, right: 0, top: 0, bottom: 0 }, + chartPaddings: { + top: 12, + bottom: 12, + }, + lineSeriesStyle: { + point: { opacity: 0 }, + }, + areaSeriesStyle: { + point: { opacity: 0 }, + }, + background: { + color: `rgba(0,0,0,0)`, + }, + axes: { + gridLine: { + horizontal: { + opacity: 1, + stroke: `rgba(0,0,0,1)`, + }, + }, + }, + }; + + const min = timeseries[0]?.x; + const max = timeseries[timeseries.length - 1]?.x; + + const defaultXFormatter = useMemo(() => { + return niceTimeFormatter([min, max]); + }, [min, max]); + + const xFormatter = givenXFormatter || defaultXFormatter; + + return ( + + { + return xFormatter(data.value); + }} + /> + + + {type && type === 'bar' ? ( + + ) : ( + + )} + {annotations?.map((annotation) => { + return ( + { + return ( + + ); + }} + /> + ); + })} + + ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts new file mode 100644 index 0000000000000..80bbd5e55cc57 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts @@ -0,0 +1,82 @@ +/* + * 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 { EuiThemeComputed } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { $Values } from 'utility-types'; +import { SignificantEventItem } from '../../hooks/use_fetch_significant_events'; +import { pValueToLabel } from './p_value_to_label'; + +type EuiThemeColor = $Values<{ + [key in keyof EuiThemeComputed['colors']]: EuiThemeComputed['colors'][key] extends string + ? key + : never; +}>; + +export interface FormattedChangePoint { + time: number; + impact: 'high' | 'medium' | 'low'; + p_value: number; + type: 'dip' | 'distribution_change' | 'spike' | 'step_change' | 'trend_change'; + label: string; + color: EuiThemeColor; +} + +function getImpactProperties(impact: FormattedChangePoint['impact']): { + color: EuiThemeColor; + label: string; +} { + if (impact === 'high') { + return { + color: 'danger', + label: i18n.translate('xpack.significantEventsTable.changePoint.dotImpactHigh', { + defaultMessage: 'High', + }), + }; + } + + if (impact === 'medium') { + return { + color: 'warning', + label: i18n.translate('xpack.significantEventsTable.changePoint.dotImpactMedium', { + defaultMessage: 'Medium', + }), + }; + } + + return { + color: 'darkShade', + label: i18n.translate('xpack.significantEventsTable.changePoint.dotImpactLow', { + defaultMessage: 'Low', + }), + }; +} + +export function formatChangePoint(item: SignificantEventItem): FormattedChangePoint | undefined { + const type = Object.keys(item.change_points.type)[0] as keyof typeof item.change_points.type; + + const isChange = type && type !== 'stationary' && type !== 'non_stationary'; + + const point = item.change_points.type[type]; + + const change = + isChange && point + ? { + type, + impact: pValueToLabel(point.p_value), + time: item.occurrences[point.change_point].x, + p_value: point.p_value, + } + : undefined; + + return change + ? { + ...change, + ...getImpactProperties(change.impact), + } + : undefined; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point_summary.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point_summary.tsx new file mode 100644 index 0000000000000..6680a8d4081d8 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point_summary.tsx @@ -0,0 +1,72 @@ +/* + * 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 { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { TickFormatter } from '@elastic/charts'; +import { FormattedChangePoint } from './change_point'; + +export function ChangePointSummary({ + change, + xFormatter, +}: { + change?: FormattedChangePoint; + xFormatter: TickFormatter; +}) { + const theme = useEuiTheme().euiTheme; + + if (!change) { + return ( + + {i18n.translate('xpack.significantEventsTable.changePointBadge.noChangesDetected', { + defaultMessage: 'No changes detected', + })} + + ); + } + + return ( + + + + + + + + + + {change.label} + + + + + {change.type} + + + + + + @ {xFormatter(change.time)} + + + + ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.stories.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.stories.tsx new file mode 100644 index 0000000000000..2793930fb92cc --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.stories.tsx @@ -0,0 +1,35 @@ +/* + * 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 { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { css } from '@emotion/css'; +import classNames from 'classnames'; +import { SignificantEventsViewEmptyState } from './empty_state'; + +const stories: Meta<{}> = { + title: 'Streams/SignificantEventsViewEmptyState', + component: SignificantEventsViewEmptyState, +}; + +export default stories; + +export const Create: StoryFn<{}> = () => { + return ( + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.tsx new file mode 100644 index 0000000000000..f7c2775fe755a --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiText, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { AssetImage } from '../asset_image'; + +export function SignificantEventsViewEmptyState({ onAddClick }: { onAddClick?: () => void }) { + return ( + + + +

+ {i18n.translate('xpack.significantEvents.emptyState.title', { + defaultMessage: 'No significant event definitions', + })} +

+
+ + {i18n.translate('xpack.significantEvents.emptyState.description', { + defaultMessage: 'There are no significant events defined for this stream yet.', + })} + + + { + onAddClick?.(); + }} + > + {i18n.translate('xpack.streams.significantEvents.emptyState.addEvent', { + defaultMessage: 'Add new event', + })} + + +
+ ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx index 76ff7dba93830..04f26da76e558 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx @@ -4,13 +4,218 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Streams } from '@kbn/streams-schema'; -import React from 'react'; +import { niceTimeFormatter } from '@elastic/charts'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { StreamQueryKql, Streams } from '@kbn/streams-schema'; +import React, { useMemo, useState } from 'react'; +import { useFetchSignificantEvents } from '../../hooks/use_fetch_significant_events'; +import { useKibana } from '../../hooks/use_kibana'; +import { useSignificantEventsApi } from '../../hooks/use_significant_events_api'; +import { useStreamsAppParams } from '../../hooks/use_streams_app_params'; +import { useTimefilter } from '../../hooks/use_timefilter'; +import { LoadingPanel } from '../loading_panel'; +import { StreamsAppSearchBar } from '../streams_app_search_bar'; +import { Timeline, TimelineEvent } from '../timeline'; +import { formatChangePoint } from './change_point'; +import { ChangePointSummary } from './change_point_summary'; +import { SignificantEventsViewEmptyState } from './empty_state'; +import { SignificantEventFlyout } from './significant_event_flyout'; +import { SignificantEventsTable } from './significant_events_table'; export function StreamDetailSignificantEventsView({ definition, }: { definition?: Streams.all.GetResponse; }) { - return <>; + const { + core: { notifications }, + } = useKibana(); + + const { + query: { kql }, + } = useStreamsAppParams('/{key}/*'); + + const { + timeState: { start, end }, + } = useTimefilter(); + + const theme = useEuiTheme().euiTheme; + + const xFormatter = useMemo(() => { + return niceTimeFormatter([start, end]); + }, [start, end]); + + const significantEventsFetchState = useFetchSignificantEvents({ + name: definition?.stream.name, + start, + end, + kql, + }); + + const { addQuery, removeQuery } = + useSignificantEventsApi({ name: definition?.stream.name }) || {}; + + const [isEditFlyoutOpen, setIsEditFlyoutOpen] = useState(false); + + const [queryToEdit, setQueryToEdit] = useState(); + + const events = useMemo(() => { + return ( + significantEventsFetchState.value?.flatMap((item): TimelineEvent[] => { + const change = formatChangePoint(item); + + if (!change) { + return []; + } + + return [ + { + id: item.query.id, + label: , + color: theme.colors[change.color], + time: change.time, + }, + ]; + }) ?? [] + ); + }, [significantEventsFetchState.value, theme, xFormatter]); + + if (!significantEventsFetchState.value) { + return ; + } + + const editFlyout = isEditFlyoutOpen ? ( + { + await addQuery?.(next).then( + () => { + notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.streams.significantEvents.significantEventCreateSuccessToastTitle', + { + defaultMessage: `Added significant event`, + } + ), + }); + setIsEditFlyoutOpen(false); + significantEventsFetchState.refresh(); + }, + (error) => { + notifications.showErrorDialog({ + title: i18n.translate( + 'xpack.streams.significantEvents.significantEventCreateErrorToastTitle', + { + defaultMessage: `Could not add significant event`, + } + ), + error, + }); + } + ); + }} + onUpdate={async (next) => { + await addQuery?.(next).then( + () => { + notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.streams.significantEvents.significantEventUpdateSuccessToastTitle', + { + defaultMessage: `Updated significant event`, + } + ), + }); + setIsEditFlyoutOpen(false); + significantEventsFetchState.refresh(); + }, + (error) => { + notifications.showErrorDialog({ + title: i18n.translate( + 'xpack.streams.significantEvents.significantEventUpdateErrorToastTitle', + { + defaultMessage: `Could not update significant event`, + } + ), + error, + }); + } + ); + }} + onClose={() => { + setIsEditFlyoutOpen(false); + setQueryToEdit(undefined); + }} + query={queryToEdit} + name={definition?.stream.name ?? ''} + /> + ) : null; + + if (significantEventsFetchState.value.length === 0) { + return ( + <> + { + setIsEditFlyoutOpen(true); + setQueryToEdit(undefined); + }} + /> + {editFlyout} + + ); + } + + return ( + <> + + + + + {}} + onQueryChange={() => {}} + /> + + + { + setIsEditFlyoutOpen(true); + setQueryToEdit(undefined); + }} + iconType="plusInCircle" + > + {i18n.translate('xpack.streams.significantEvents.addSignificantEventButton', { + defaultMessage: 'Add significant event query', + })} + {' '} + + + + + + + + + + { + setIsEditFlyoutOpen(true); + setQueryToEdit(item.query); + }} + onDeleteClick={async (item) => { + await removeQuery?.(item.query.id).then(() => { + significantEventsFetchState.refresh(); + }); + }} + xFormatter={xFormatter} + /> + + + {editFlyout} + + ); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/p_value_to_label.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/p_value_to_label.ts new file mode 100644 index 0000000000000..3f6e0836d129b --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/p_value_to_label.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export const P_VALUE_SIGNIFICANCE_HIGH = 1e-6; +export const P_VALUE_SIGNIFICANCE_MEDIUM = 0.001; + +export function pValueToLabel(pValue: number): 'high' | 'medium' | 'low' { + if (pValue <= P_VALUE_SIGNIFICANCE_HIGH) { + return 'high'; + } else if (pValue <= P_VALUE_SIGNIFICANCE_MEDIUM) { + return 'medium'; + } else { + return 'low'; + } +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout.tsx new file mode 100644 index 0000000000000..a5ba1ff47735a --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout.tsx @@ -0,0 +1,384 @@ +/* + * 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 { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiForm, + EuiFormLabel, + EuiFormRow, + EuiSpacer, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import React, { useMemo, useState } from 'react'; +import { StreamQueryKql, streamQuerySchema } from '@kbn/streams-schema'; +import { i18n } from '@kbn/i18n'; +import { v4 } from 'uuid'; +import { fromKueryExpression } from '@kbn/es-query'; +import moment from 'moment'; +import { calculateAuto } from '@kbn/calculate-auto'; +import { getAbsoluteTimeRange } from '@kbn/data-plugin/common'; +import { useKibana } from '../../hooks/use_kibana'; +import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch'; +import { SparkPlot } from '../spark_plot'; +import { formatChangePoint } from './change_point'; +import { getAnnotationFromFormattedChangePoint } from './utils/get_annotation_from_formatted_change_point'; +import { useTimefilter } from '../../hooks/use_timefilter'; +import { UncontrolledStreamsAppSearchBar } from '../streams_app_search_bar/uncontrolled_streams_app_bar'; + +function getTitle(query?: StreamQueryKql) { + if (!query) { + return i18n.translate('xpack.significantEventFlyout.addNewQueryFlyoutTitle', { + defaultMessage: 'Add significant event', + }); + } + + return i18n.translate('xpack.significantEventFlyout.editQueryFlyoutTitle', { + defaultMessage: 'Edit {title}', + values: { + title: query.title, + }, + }); +} + +function getSubmitTitle(query?: StreamQueryKql) { + if (!query) { + return i18n.translate('xpack.significantEventFlyout.addButtonLabel', { + defaultMessage: 'Add', + }); + } + + return i18n.translate('xpack.significantEventFlyout.editButtonLabel', { + defaultMessage: 'Save changes', + values: { + title: query.title, + }, + }); +} + +interface SignificantEventFlyoutProps { + name: string; + onClose?: () => void; + onCreate?: (query: StreamQueryKql) => Promise; + onUpdate?: (query: StreamQueryKql) => Promise; + query?: StreamQueryKql; +} + +export function SignificantEventFlyoutContents({ + name, + query, + onClose, + onCreate, + onUpdate, +}: SignificantEventFlyoutProps) { + const [queryValues, setQueryValues] = useState<{ id: string } & Partial>({ + id: v4(), + ...query, + }); + + const [touched, setTouched] = useState({ title: false, kql: false }); + + const [loading, setLoading] = useState(false); + + const theme = useEuiTheme().euiTheme; + + const { + dependencies: { + start: { data, streams }, + }, + } = useKibana(); + + const { + timeState: { timeRange: initialTimeRange }, + } = useTimefilter(); + + const [timeRange, setTimeRange] = useState(initialTimeRange); + + const dataViewsFetch = useStreamsAppFetch(() => { + return data.dataViews + .create({ + title: name, + }) + .then((value) => { + return [value]; + }); + }, [data.dataViews, name]); + + const validation = useMemo(() => { + const { title = '', kql: { query: kqlQuery } = { query: '' } } = queryValues; + const titleEmptyError = title.length === 0; + const kqlEmptyError = kqlQuery.length === 0; + + const titleErrorMessage = titleEmptyError + ? i18n.translate('xpack.significantEventFlyout.formFieldTitleRequiredError', { + defaultMessage: 'Required', + }) + : undefined; + + let kqlSyntaxError = false; + + if (!kqlEmptyError) { + try { + fromKueryExpression(kqlQuery); + } catch (error) { + kqlSyntaxError = true; + } + } + + const kqlErrorMessage = kqlSyntaxError + ? i18n.translate('xpack.significantEventFlyout.formFieldQuerySyntaxError', { + defaultMessage: 'Invalid syntax', + }) + : kqlEmptyError + ? i18n.translate('xpack.significantEventFlyout.formFieldQueryRequiredError', { + defaultMessage: 'Required', + }) + : undefined; + + return { + title: titleErrorMessage, + kql: kqlErrorMessage, + }; + }, [queryValues]); + + const validationMessages = useMemo(() => { + return { + title: + validation.title && touched.title + ? { + isInvalid: true, + error: validation.title, + } + : {}, + kql: + validation.kql && touched.kql + ? { + isInvalid: true, + error: validation.kql, + } + : {}, + }; + }, [validation, touched]); + + const previewFetch = useStreamsAppFetch( + ({ signal }) => { + const { id, kql, title } = queryValues; + if (!id || !kql?.query || !title) { + return; + } + + const { from, to } = getAbsoluteTimeRange(timeRange); + + const bucketSize = calculateAuto + .near(50, moment.duration(moment(to).diff(from))) + ?.asSeconds()!; + + return streams.streamsRepositoryClient.fetch( + `POST /api/streams/{name}/significant_events/_preview`, + { + signal, + params: { + path: { + name, + }, + query: { + bucketSize: `${bucketSize}s`, + from, + to, + }, + body: { + query: { + id, + kql, + title, + }, + }, + }, + } + ); + }, + [timeRange, name, queryValues, streams.streamsRepositoryClient] + ); + + const sparkPlotData = useMemo(() => { + const changePoints = previewFetch.value?.change_points; + const occurrences = previewFetch.value?.occurrences; + + const timeseries = + occurrences?.map(({ date, count }) => { + return { + x: new Date(date).getTime(), + y: count, + }; + }) ?? []; + + const { id, kql, title } = queryValues; + + const change = + changePoints && occurrences && id && kql && title + ? formatChangePoint({ + change_points: changePoints, + occurrences: timeseries, + query: { + id, + kql, + title, + }, + }) + : undefined; + + return { + timeseries, + annotations: change + ? [ + getAnnotationFromFormattedChangePoint({ + query: { + id, + }, + change, + theme, + }), + ] + : [], + }; + }, [previewFetch.value, queryValues, theme]); + + const parsedQuery = useMemo(() => { + return streamQuerySchema.safeParse(queryValues); + }, [queryValues]); + + return ( + <> + + +

{getTitle(query)}

+
+
+ + + + {i18n.translate('xpack.significantEventFlyout.formFieldTitleLabel', { + defaultMessage: 'Title', + })} + + } + > + { + const next = event.currentTarget.value; + setQueryValues((prev) => ({ ...prev, title: next })); + setTouched((prev) => ({ ...prev, title: true })); + }} + /> + + + {i18n.translate('xpack.significantEventFlyout.formFieldQueryLabel', { + defaultMessage: 'Query', + })} + + } + {...validationMessages.kql} + > + { + setTouched((prev) => ({ ...prev, kql: true })); + }} + onQuerySubmit={(next) => { + setQueryValues((prev) => ({ ...prev, kql: { query: next.query } })); + setTouched((prev) => ({ ...prev, kql: true })); + if (next.dateRange) { + setTimeRange(next.dateRange); + } + }} + dateRangeFrom={timeRange.from} + dateRangeTo={timeRange.to} + placeholder={i18n.translate('xpack.significantEventFlyout.queryPlaceholder', { + defaultMessage: 'Filter events', + })} + dataViews={dataViewsFetch.value} + /> + + + + + + + + { + onClose?.(); + }} + > + {i18n.translate('xpack.significantEventFlyout.cancelButtonLabel', { + defaultMessage: 'Cancel', + })} + + { + if (!parsedQuery.success) { + return; + } + + setLoading(true); + + if (query) { + onUpdate?.(parsedQuery.data).finally(() => { + setLoading(false); + }); + } else { + onCreate?.(parsedQuery.data).finally(() => { + setLoading(false); + }); + } + }} + > + {getSubmitTitle(query)} + + + + + ); +} +export function SignificantEventFlyout(props: SignificantEventFlyoutProps) { + return ( + { + props.onClose?.(); + }} + size="m" + > + + + ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_flyout.stories.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_flyout.stories.tsx new file mode 100644 index 0000000000000..040d263281db4 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_flyout.stories.tsx @@ -0,0 +1,35 @@ +/* + * 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 { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { css } from '@emotion/css'; +import classNames from 'classnames'; +import { SignificantEventFlyout } from './significant_event_flyout'; + +const stories: Meta<{}> = { + title: 'Streams/SignificantEventFlyout', + component: SignificantEventFlyout, +}; + +export default stories; + +export const Create: StoryFn<{}> = () => { + return ( + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_histogram.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_histogram.tsx new file mode 100644 index 0000000000000..08f2a7594df9b --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_histogram.tsx @@ -0,0 +1,51 @@ +/* + * 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 { useEuiTheme } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { TickFormatter } from '@elastic/charts'; +import { SparkPlot, SparkPlotAnnotation } from '../spark_plot'; +import { FormattedChangePoint } from './change_point'; +import { getAnnotationFromFormattedChangePoint } from './utils/get_annotation_from_formatted_change_point'; + +interface Props { + id: string; + occurrences: Array<{ x: number; y: number }>; + change?: FormattedChangePoint; + xFormatter?: TickFormatter; +} + +export function SignificantEventsHistogramChart({ id, occurrences, change, xFormatter }: Props) { + const theme = useEuiTheme().euiTheme; + + const annotations = useMemo((): SparkPlotAnnotation[] => { + if (!change) { + return []; + } + return [ + getAnnotationFromFormattedChangePoint({ + query: { id }, + change, + theme, + }), + ]; + }, [change, id, theme]); + + return ( + + ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.stories.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.stories.tsx new file mode 100644 index 0000000000000..e5f98a58533b7 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.stories.tsx @@ -0,0 +1,86 @@ +/* + * 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 { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; +import { niceTimeFormatter } from '@elastic/charts'; +import { SignificantEventsTable } from './significant_events_table'; + +const stories: Meta<{}> = { + title: 'Streams/SignificantEventsTable', + component: SignificantEventsTable, +}; + +export default stories; + +const start = new Date(`2025-03-24T12:00:00.000Z`).getTime(); +const end = new Date(`2025-03-24T14:00:00.000Z`).getTime(); + +function generateValues() { + return new Array(25).fill(undefined).map((_, index) => { + return { + x: start + index * 60 * 1000, + y: 100 + (12 - Math.abs(index - 12)) * 10, + }; + }); +} + +const xFormatter = niceTimeFormatter([start, end]); + +export const Empty: StoryFn<{}> = () => { + return ( + + ); +}; + +export const SomeThings: StoryFn<{}> = () => { + return ( + { + return new Promise((resolve) => + setTimeout(() => { + resolve(); + }, 1000) + ); + }} + response={{ + loading: false, + value: [ + { + query: { + id: 'match_everything', + title: 'Match everything', + kql: { + query: '*', + }, + }, + change_points: { + type: { + spike: { + change_point: 3, + p_value: 0.0001, + }, + }, + }, + occurrences: generateValues(), + }, + ], + error: undefined, + }} + xFormatter={xFormatter} + /> + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.tsx new file mode 100644 index 0000000000000..0c4d8e8569bd9 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.tsx @@ -0,0 +1,169 @@ +/* + * 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 { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AbortableAsyncState } from '@kbn/react-hooks'; +import React, { useMemo, useState } from 'react'; +import { TickFormatter } from '@elastic/charts'; +import { SignificantEventItem } from '../../hooks/use_fetch_significant_events'; +import { useKibana } from '../../hooks/use_kibana'; +import { formatChangePoint } from './change_point'; +import { ChangePointSummary } from './change_point_summary'; +import { SignificantEventsHistogramChart } from './significant_events_histogram'; +import { buildDiscoverParams } from './utils/discover_helpers'; + +function WithLoadingSpinner({ onClick, ...props }: React.ComponentProps) { + const [isLoading, setIsLoading] = useState(false); + + return ( + & React.MouseEvent + ) => { + setIsLoading(true); + Promise.resolve(onClick?.(event)).finally(() => { + setIsLoading(false); + }); + }} + /> + ); +} + +export function SignificantEventsTable({ + name, + response, + onDeleteClick, + onEditClick, + xFormatter, +}: { + name?: string; + response: Pick, 'value' | 'loading' | 'error'>; + onDeleteClick?: (query: SignificantEventItem) => void; + onEditClick?: (query: SignificantEventItem) => void; + xFormatter: TickFormatter; +}) { + const { + dependencies: { + start: { discover }, + }, + } = useKibana(); + + const items = useMemo(() => { + return response.value ?? []; + }, [response.value]); + + const columns: Array> = [ + { + field: 'title', + name: i18n.translate('xpack.streams.significantEventsTable.titleColumnTitle', { + defaultMessage: 'Title', + }), + render: (_, record) => ( + + {record.query.title} + + ), + }, + { + field: 'change', + name: i18n.translate('xpack.streams.significantEventsTable.changeColumnTitle', { + defaultMessage: 'Change', + }), + render: (_, item) => { + const change = formatChangePoint(item); + return ; + }, + }, + { + field: 'occurrences', + name: i18n.translate('xpack.streams.significantEventsTable.occurrencesColumnTitle', { + defaultMessage: 'Occurrences', + }), + render: (_, item) => { + const change = formatChangePoint(item); + return ( + + ); + }, + }, + { + name: i18n.translate('xpack.streams.significantEventsTable.actionsColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.streams.significantEventsTable.editQueryActionTitle', { + defaultMessage: 'Edit', + }), + description: i18n.translate( + 'xpack.streams.significantEventsTable.editQueryActionDescription', + { + defaultMessage: 'Edit query', + } + ), + render: (item) => { + return ( + { + return onEditClick?.(item); + }} + /> + ); + }, + }, + { + name: i18n.translate('xpack.streams.significantEventsTable.removeQueryActionTitle', { + defaultMessage: 'Remove', + }), + description: i18n.translate( + 'xpack.streams.significantEventsTable.removeQueryActionDescription', + { + defaultMessage: 'Remove query from stream', + } + ), + render: (item) => { + return ( + { + return onDeleteClick?.(item); + }} + /> + ); + }, + }, + ], + }, + ]; + + return ( + + ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/discover_helpers.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/discover_helpers.ts new file mode 100644 index 0000000000000..bd18bb5ca4389 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/discover_helpers.ts @@ -0,0 +1,27 @@ +/* + * 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 { v4 } from 'uuid'; +import { SignificantEventItem } from '../../../hooks/use_fetch_significant_events'; + +export function buildDiscoverParams(significantEvent: SignificantEventItem, name?: string) { + return { + timeRange: { + from: 'now-7d', + to: 'now', + }, + query: { + query: significantEvent.query.kql.query, + language: 'kuery', + }, + dataViewSpec: { + id: v4(), + title: name, + timeFieldName: '@timestamp', + }, + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/get_annotation_from_formatted_change_point.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/get_annotation_from_formatted_change_point.tsx new file mode 100644 index 0000000000000..49308b0940a06 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/get_annotation_from_formatted_change_point.tsx @@ -0,0 +1,28 @@ +/* + * 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 React from 'react'; +import { EuiIcon, EuiThemeComputed } from '@elastic/eui'; +import { FormattedChangePoint } from '../change_point'; + +export function getAnnotationFromFormattedChangePoint({ + query: { id }, + theme, + change, +}: { + theme: EuiThemeComputed; + change: FormattedChangePoint; + query: { id: string }; +}) { + const color = theme.colors[change?.color]; + return { + color, + icon: , + id: `change_point_${id}`, + label: change.label, + x: change.time, + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx index 6e62589b0ba9a..cd69275b6568e 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx @@ -8,21 +8,26 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiBadgeGroup, EuiButton } from '@elastic/eui'; import { Streams } from '@kbn/streams-schema'; +import type { ILicense } from '@kbn/licensing-plugin/public'; import { useStreamsAppParams } from '../../hooks/use_streams_app_params'; import { StreamDetailDashboardsView } from '../stream_detail_dashboards_view'; import { StreamDetailOverview } from '../stream_detail_overview'; +import { StreamDetailSignificantEventsView } from '../stream_detail_significant_events_view'; import { useStreamDetail } from '../../hooks/use_stream_detail'; import { ClassicStreamBadge, LifecycleBadge } from '../stream_badges'; import { StreamsAppPageTemplate } from '../streams_app_page_template'; import { StatefulStreamsAppRouter, useStreamsAppRouter } from '../../hooks/use_streams_app_router'; import { RedirectTo } from '../redirect_to'; +import { useStreamPrivileges } from '../../hooks/use_stream_privileges'; const getStreamDetailTabs = ({ definition, router, + license, }: { definition: Streams.ingest.all.GetResponse; router: StatefulStreamsAppRouter; + license: ILicense; }) => ({ overview: { @@ -45,13 +50,27 @@ const getStreamDetailTabs = ({ defaultMessage: 'Dashboards', }), }, + ...(license.hasAtLeast('enterprise') + ? { + significant_events: { + href: router.link('/{key}/{tab}', { + path: { key: definition.stream.name, tab: 'significant_events' }, + }), + content: , + label: i18n.translate('xpack.streams.streamDetailView.significantEventsTab', { + defaultMessage: 'Significant events', + }), + background: true, + }, + } + : {}), } as const); export type StreamDetailTabs = ReturnType; export type StreamDetailTabName = keyof StreamDetailTabs; function isValidStreamDetailTab(value: string): value is StreamDetailTabName { - return ['overview', 'dashboards'].includes(value as StreamDetailTabName); + return ['overview', 'dashboards', 'significant_events'].includes(value as StreamDetailTabName); } export function StreamDetailView() { @@ -61,6 +80,8 @@ export function StreamDetailView() { const { definition } = useStreamDetail(); + const { license } = useStreamPrivileges(); + if (tab === 'management') { return ; } @@ -69,9 +90,9 @@ export function StreamDetailView() { return ; } - const tabs = getStreamDetailTabs({ definition, router }); + const tabs = license ? getStreamDetailTabs({ definition, router, license }) : undefined; - const selectedTabObject = tabs[tab as StreamDetailTabName]; + const selectedTabObject = tabs?.[tab as StreamDetailTabName]; return ( <> @@ -86,7 +107,7 @@ export function StreamDetailView() { } - tabs={Object.entries(tabs).map(([tabName, { label, href }]) => { + tabs={Object.entries(tabs ?? {}).map(([tabName, { label, href }]) => { return { label, href, @@ -106,8 +127,8 @@ export function StreamDetailView() { , ]} /> - - {selectedTabObject.content} + + {selectedTabObject?.content} ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/index.tsx index a77d609a695ea..5a2fd8ff56950 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/index.tsx @@ -18,6 +18,7 @@ export interface StreamsAppSearchBarProps { onQuerySubmit?: (payload: { query: string }) => void; placeholder?: string; dataViews?: DataView[]; + showQueryInput?: boolean; showSubmitButton?: boolean; showDatePicker?: boolean; } @@ -39,6 +40,7 @@ export function StreamsAppSearchBar({ dataViews, showDatePicker = false, showSubmitButton = true, + showQueryInput, }: StreamsAppSearchBarProps) { const { timeState, setTime, refresh } = useTimefilter(); @@ -62,6 +64,7 @@ export function StreamsAppSearchBar({ onQueryChange?.({ query: nextQuery }); }} query={query} + showQueryInput={showQueryInput} showSubmitButton={showSubmitButton} dateRangeFrom={showDatePicker ? timeState.timeRange.from : undefined} dateRangeTo={showDatePicker ? timeState.timeRange.to : undefined} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx index 962f502d32bac..f717dfb77daf3 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx @@ -20,6 +20,7 @@ export interface UncontrolledStreamsAppSearchBarProps { placeholder?: string; dataViews?: DataView[]; showSubmitButton?: boolean; + showQueryInput?: boolean; } export function UncontrolledStreamsAppSearchBar({ @@ -32,6 +33,7 @@ export function UncontrolledStreamsAppSearchBar({ placeholder, dataViews, showSubmitButton = true, + showQueryInput, }: UncontrolledStreamsAppSearchBarProps) { const { dependencies: { @@ -39,9 +41,10 @@ export function UncontrolledStreamsAppSearchBar({ }, } = useKibana(); - const queryObj = useMemo(() => (query ? { query, language: 'kuery' } : undefined), [query]); - - const showQueryInput = query === undefined; + const queryObj = useMemo( + () => (showQueryInput ? { query: query ?? '', language: 'kuery' } : undefined), + [query, showQueryInput] + ); return ( = { + title: 'Streams/Timeline', + component: Timeline, +}; + +export default stories; + +const start = new Date(`2025-03-24T12:00:00.000Z`).getTime(); +const end = new Date(`2025-03-24T14:00:00.000Z`).getTime(); + +const xFormatter = niceTimeFormatter([start, end]); + +export const Empty: StoryFn<{}> = () => { + return ( + + + + ); +}; + +export const WithEvents: StoryFn<{}> = () => { + const theme = useEuiTheme(); + + const events = [ + { + id: 'some_event', + time: new Date(`2025-03-24T12:30:00.000Z`).getTime(), + label: `Some event`, + color: theme.euiTheme.colors.danger, + }, + { + id: 'some_other_event', + time: new Date(`2025-03-24T12:10:00.000Z`).getTime(), + label: `Some other event`, + color: theme.euiTheme.colors.danger, + }, + { + id: 'another_event', + time: new Date(`2025-03-24T13:48:00.000Z`).getTime(), + label: `Another event`, + color: theme.euiTheme.colors.danger, + }, + ]; + return ( + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.tsx new file mode 100644 index 0000000000000..ca109461a4431 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.tsx @@ -0,0 +1,185 @@ +/* + * 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 { + AnnotationDomainType, + Axis, + Chart, + LineAnnotation, + LineSeries, + PartialTheme, + Settings, + TickFormatter, + Tooltip, + TooltipContainer, + TooltipTable, + TooltipTableBody, + TooltipTableCell, + TooltipTableColorCell, + TooltipTableRow, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiTitle, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { calculateAuto } from '@kbn/calculate-auto'; +import { i18n } from '@kbn/i18n'; +import { range } from 'lodash'; +import moment from 'moment'; +import React, { useMemo } from 'react'; +import { useKibana } from '../../hooks/use_kibana'; + +export interface TimelineEvent { + id: string; + time: number; + label: React.ReactNode; + color: string; +} + +interface TimelineProps { + events: TimelineEvent[]; + start: number; + end: number; + xFormatter: TickFormatter; +} + +export function Timeline({ events, start, end, xFormatter }: TimelineProps) { + const { + dependencies: { + start: { charts }, + }, + } = useKibana(); + + const theme = useEuiTheme().euiTheme; + + const baseTheme = charts.theme.useChartsBaseTheme(); + const defaultTheme = charts.theme.chartsDefaultBaseTheme; + + const minimalChartTheme: PartialTheme = { + chartMargins: { left: 0, right: 0, top: 0, bottom: 0 }, + chartPaddings: { + top: 12, + bottom: 12, + }, + lineSeriesStyle: { + fit: { + line: { + opacity: 0, + }, + }, + point: { opacity: 0 }, + }, + areaSeriesStyle: { + point: { opacity: 0 }, + }, + }; + + const dummyValues = useMemo(() => { + if (events.length) { + return events.map((event) => { + return { + x: event.time, + }; + }); + } + const delta = calculateAuto.atLeast(20, moment.duration(end - start))?.asMilliseconds()!; + + const buckets = Math.floor((end - start) / delta); + + return range(0, buckets).map((index) => { + return { + x: start + index * delta, + }; + }); + }, [start, end, events]); + + return ( + + + + +

+ {i18n.translate('xpack.streams.timeline.title', { + defaultMessage: 'Timeline', + })} +

+
+
+ + } + /> + + + + + ({ dataValue: event.time, event }))} + domainType={AnnotationDomainType.XDomain} + marker={(point) => { + const { event } = point as { dataValue: number; event: TimelineEvent }; + return ( + + ); + }} + customTooltip={({ datum }) => { + const { event } = datum as { dataValue: number; event: TimelineEvent }; + return ( + + + + + + {event.label} + + + + + ); + }} + /> + +
+
+ ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_fetch_significant_events.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_fetch_significant_events.ts new file mode 100644 index 0000000000000..8b5a2da31cfa5 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_fetch_significant_events.ts @@ -0,0 +1,97 @@ +/* + * 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 { calculateAuto } from '@kbn/calculate-auto'; +import moment from 'moment'; +import { SignificantEventsResponse, StreamQuery } from '@kbn/streams-schema'; +import { useKibana } from './use_kibana'; +import { useStreamsAppFetch } from './use_streams_app_fetch'; + +export interface SignificantEventItem { + query: StreamQuery; + occurrences: Array<{ x: number; y: number }>; + change_points: SignificantEventsResponse['change_points']; +} + +export const useFetchSignificantEvents = ({ + name, + start, + end, + kql, +}: { + name?: string; + start: number; + end: number; + kql?: string; +}) => { + const { + dependencies: { + start: { + streams: { streamsRepositoryClient }, + data, + }, + }, + } = useKibana(); + + const result = useStreamsAppFetch( + async ({ signal }): Promise => { + if (!name) { + return Promise.resolve(undefined); + } + + const isoFrom = new Date(start).toISOString(); + const isoTo = new Date(end).toISOString(); + + const { min, max } = data.query.timefilter.timefilter.calculateBounds({ + from: isoFrom, + to: isoTo, + }); + + if (!min || !max) { + return undefined; + } + + const bucketSize = calculateAuto.near(50, moment.duration(max.diff(min))); + if (!bucketSize) { + return undefined; + } + + const intervalString = `${bucketSize.asSeconds()}s`; + + const response = await streamsRepositoryClient + .fetch('GET /api/streams/{name}/significant_events 2023-10-31', { + params: { + path: { name }, + query: { + from: isoFrom, + to: isoTo, + bucketSize: intervalString, + }, + }, + signal, + }) + .then((res) => { + return res.map((series) => { + const { occurrences, change_points: changePoints, ...query } = series; + return { + query, + change_points: changePoints, + occurrences: occurrences.map((occurrence) => ({ + x: new Date(occurrence.date).getTime(), + y: occurrence.count, + })), + }; + }); + }); + + return response; + }, + [name, start, end, streamsRepositoryClient, data.query.timefilter] + ); + + return result; +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_significant_events_api.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_significant_events_api.ts new file mode 100644 index 0000000000000..f6ea1ee3f0d8d --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_significant_events_api.ts @@ -0,0 +1,99 @@ +/* + * 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 { StreamQueryKql } from '@kbn/streams-schema'; +import { useMemo } from 'react'; +import { useAbortController } from '@kbn/react-hooks'; +import { useKibana } from './use_kibana'; + +interface SignificantEventsApiBulkOperationCreate { + index: StreamQueryKql; +} +interface SignificantEventsApiBulkOperationDelete { + delete: { id: string }; +} + +type SignificantEventsApiBulkOperation = + | SignificantEventsApiBulkOperationCreate + | SignificantEventsApiBulkOperationDelete; + +interface SignificantEventsApi { + addQuery: (query: StreamQueryKql) => Promise; + removeQuery: (id: string) => Promise; + bulk: (operations: SignificantEventsApiBulkOperation[]) => Promise; +} + +export function useSignificantEventsApi({ + name, +}: { + name?: string; +}): SignificantEventsApi | undefined { + const { + dependencies: { + start: { + streams: { streamsRepositoryClient }, + }, + }, + } = useKibana(); + + const { signal } = useAbortController(); + + return useMemo(() => { + return !name + ? undefined + : { + addQuery: async ({ kql, title, id }) => { + await streamsRepositoryClient.fetch( + 'PUT /api/streams/{name}/queries/{queryId} 2023-10-31', + { + signal, + params: { + path: { + name, + queryId: id, + }, + body: { + kql, + title, + }, + }, + } + ); + }, + removeQuery: async (id) => { + await streamsRepositoryClient.fetch( + 'DELETE /api/streams/{name}/queries/{queryId} 2023-10-31', + { + signal, + params: { + path: { + name, + queryId: id, + }, + }, + } + ); + }, + bulk: async (operations) => { + await streamsRepositoryClient.fetch( + 'POST /api/streams/{name}/queries/_bulk 2023-10-31', + { + signal, + params: { + path: { + name, + }, + body: { + operations, + }, + }, + } + ); + }, + }; + }, [name, signal, streamsRepositoryClient]); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_privileges.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_privileges.ts new file mode 100644 index 0000000000000..0dd8f2efcacfe --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_privileges.ts @@ -0,0 +1,37 @@ +/* + * 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_UI_PRIVILEGES } from '@kbn/streams-plugin/public'; +import useObservable from 'react-use/lib/useObservable'; +import type { ILicense } from '@kbn/licensing-plugin/public'; +import { useKibana } from './use_kibana'; + +export function useStreamPrivileges(): { + ui: { manage: boolean; show: boolean }; + license: ILicense | undefined; +} { + const { + core: { + application: { + capabilities: { streams }, + }, + }, + dependencies: { + start: { licensing }, + }, + } = useKibana(); + + const license = useObservable(licensing.license$); + + return { + ui: streams as { + [STREAMS_UI_PRIVILEGES.manage]: boolean; + [STREAMS_UI_PRIVILEGES.show]: boolean; + }, + license, + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/routes/config.tsx b/x-pack/platform/plugins/shared/streams_app/public/routes/config.tsx index f21f34e693a57..a407a1ef175a8 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/routes/config.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/routes/config.tsx @@ -44,11 +44,18 @@ const streamsAppRoutes = { ), - params: t.type({ - path: t.type({ - key: t.string, + params: t.intersection([ + t.type({ + path: t.type({ + key: t.string, + }), + }), + t.partial({ + query: t.partial({ + kql: t.string, + }), }), - }), + ]), children: { '/{key}': { element: , diff --git a/x-pack/platform/plugins/shared/streams_app/public/types.ts b/x-pack/platform/plugins/shared/streams_app/public/types.ts index d52e9487361a4..77e7ff628396c 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/types.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/types.ts @@ -4,24 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ChartsPluginStart } from '@kbn/charts-plugin/public'; -import { AppMountParameters } from '@kbn/core/public'; +import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; +import type { AppMountParameters } from '@kbn/core/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginSetup, DataViewsPublicPluginStart, } from '@kbn/data-views-plugin/public'; -import { DiscoverStart } from '@kbn/discover-plugin/public'; -import { +import type { DiscoverSharedPublicSetup, DiscoverSharedPublicStart, } from '@kbn/discover-shared-plugin/public'; -import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; -import { IndexManagementPluginStart } from '@kbn/index-management-shared-types'; -import { IngestPipelinesPluginStart } from '@kbn/ingest-pipelines-plugin/public'; -import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; -import { NavigationPublicStart } from '@kbn/navigation-plugin/public/types'; -import { +import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; +import type { IndexManagementPluginStart } from '@kbn/index-management-shared-types'; +import type { IngestPipelinesPluginStart } from '@kbn/ingest-pipelines-plugin/public'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import type { NavigationPublicStart } from '@kbn/navigation-plugin/public/types'; +import type { ObservabilityAIAssistantPublicSetup, ObservabilityAIAssistantPublicStart, } from '@kbn/observability-ai-assistant-plugin/public'; @@ -29,6 +28,7 @@ import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-p import type { SharePublicSetup, SharePublicStart } from '@kbn/share-plugin/public/plugin'; import type { StreamsPluginStart } from '@kbn/streams-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { DiscoverStart } from '@kbn/discover-plugin/public'; /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ConfigSchema {} @@ -43,6 +43,7 @@ export type StreamsApplicationComponentType = React.FC; export interface StreamsAppSetupDependencies { data: DataPublicPluginSetup; dataViews: DataViewsPublicPluginSetup; + discover: DiscoverStart; discoverShared: DiscoverSharedPublicSetup; observabilityAIAssistant: ObservabilityAIAssistantPublicSetup; share: SharePublicSetup; diff --git a/x-pack/platform/plugins/shared/streams_app/tsconfig.json b/x-pack/platform/plugins/shared/streams_app/tsconfig.json index 6aad9554a4139..720aeb85d75d0 100644 --- a/x-pack/platform/plugins/shared/streams_app/tsconfig.json +++ b/x-pack/platform/plugins/shared/streams_app/tsconfig.json @@ -13,61 +13,58 @@ ], "exclude": ["target/**/*", ".storybook/**/*.js"], "kbn_references": [ - "@kbn/i18n", - "@kbn/streams-plugin", - "@kbn/core", - "@kbn/data-plugin", - "@kbn/data-views-plugin", - "@kbn/unified-search-plugin", + "@kbn/streams-schema", + "@kbn/utility-types", "@kbn/share-plugin", - "@kbn/navigation-plugin", - "@kbn/saved-objects-tagging-plugin", + "@kbn/core", + "@kbn/react-kibana-context-render", "@kbn/shared-ux-link-redirect-app", "@kbn/typed-react-router-config", - "@kbn/react-kibana-context-render", - "@kbn/code-editor", - "@kbn/ui-theme", - "@kbn/charts-plugin", - "@kbn/discover-plugin", + "@kbn/data-plugin", + "@kbn/dataset-quality-plugin", + "@kbn/discover-shared-plugin", + "@kbn/fields-metadata-plugin", + "@kbn/index-management-shared-types", + "@kbn/ingest-pipelines-plugin", "@kbn/kibana-react-plugin", - "@kbn/es-query", - "@kbn/server-route-repository-client", - "@kbn/logging", - "@kbn/config-schema", - "@kbn/streams-plugin", - "@kbn/share-plugin", + "@kbn/kibana-utils-plugin", + "@kbn/licensing-plugin", + "@kbn/observability-ai-assistant-plugin", + "@kbn/unified-search-plugin", + "@kbn/i18n", "@kbn/code-editor", - "@kbn/ui-theme", - "@kbn/navigation-plugin", - "@kbn/index-lifecycle-management-common-shared", - "@kbn/streams-schema", - "@kbn/react-hooks", "@kbn/i18n-react", + "@kbn/react-hooks", + "@kbn/react-kibana-mount", "@kbn/react-field", "@kbn/shared-ux-utility", "@kbn/unsaved-changes-prompt", - "@kbn/deeplinks-analytics", - "@kbn/dashboard-plugin", - "@kbn/react-kibana-mount", - "@kbn/fields-metadata-plugin", - "@kbn/observability-ai-assistant-plugin", "@kbn/actions-plugin", - "@kbn/dataset-quality-plugin", - "@kbn/search-types", + "@kbn/streams-plugin", "@kbn/object-utils", - "@kbn/traced-es-client", - "@kbn/licensing-plugin", - "@kbn/datemath", "@kbn/xstate-utils", + "@kbn/es-query", + "@kbn/datemath", + "@kbn/search-types", + "@kbn/index-lifecycle-management-common-shared", + "@kbn/charts-theme", + "@kbn/traced-es-client", + "@kbn/content-packs-schema", + "@kbn/dashboard-plugin", + "@kbn/deeplinks-analytics", "@kbn/visualization-utils", - "@kbn/utility-types", + "@kbn/calculate-auto", + "@kbn/ui-theme", + "@kbn/deeplinks-observability", + "@kbn/data-views-plugin", "@kbn/discover-utils", - "@kbn/discover-shared-plugin", + "@kbn/server-route-repository-client", + "@kbn/logging", "@kbn/core-analytics-browser", - "@kbn/index-management-shared-types", - "@kbn/ingest-pipelines-plugin", - "@kbn/deeplinks-observability", - "@kbn/content-packs-schema", - "@kbn/charts-theme" + "@kbn/charts-plugin", + "@kbn/navigation-plugin", + "@kbn/saved-objects-tagging-plugin", + "@kbn/discover-plugin", + "@kbn/config-schema", ] } diff --git a/x-pack/solutions/observability/packages/utils-server/tsconfig.json b/x-pack/solutions/observability/packages/utils-server/tsconfig.json index f7cf4e0940bc0..6501a98da1ef0 100644 --- a/x-pack/solutions/observability/packages/utils-server/tsconfig.json +++ b/x-pack/solutions/observability/packages/utils-server/tsconfig.json @@ -16,14 +16,14 @@ "target/**/*" ], "kbn_references": [ - "@kbn/es-types", "@kbn/es-query", - "@kbn/observability-utils-common", "@kbn/traced-es-client", "@kbn/alerting-plugin", "@kbn/rule-registry-plugin", "@kbn/rule-data-utils", + "@kbn/observability-utils-common", "@kbn/aiops-log-pattern-analysis", + "@kbn/es-types", "@kbn/calculate-auto", ] } diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/tsconfig.json b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/tsconfig.json index fc44d273a9efe..42e883379497a 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/tsconfig.json @@ -86,7 +86,7 @@ "@kbn/i18n-react", "@kbn/utility-types", "@kbn/alerts-ui-shared", - "@kbn/traced-es-client" + "@kbn/traced-es-client", ], "exclude": ["target/**/*"] } From 55120bb23bfa420713ad98c3b5a9decd4ff9b8b2 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Sun, 8 Jun 2025 16:13:07 +0200 Subject: [PATCH 02/17] Fix type issues --- .../packages/shared/kbn-streams-schema/index.ts | 7 +------ .../change_point.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 10 deletions(-) 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 18d627f52ed63..aaccff718b8d5 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts @@ -140,12 +140,7 @@ export { export type { SignificantEventsResponse, SignificantEventsGetResponse, + SignificantEventsPreviewResponse, } from './src/api/significant_events'; export { conditionToQueryDsl } from './src/helpers/condition_to_query_dsl'; - -export type { - SignificantEventsGetResponse, - SignificantEventsPreviewResponse, - SignificantEventsResponse, -} from './src/api/significant_events/api'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts index 80bbd5e55cc57..79a0b206fa51e 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts @@ -63,13 +63,16 @@ export function formatChangePoint(item: SignificantEventItem): FormattedChangePo const point = item.change_points.type[type]; + const pValue = point?.p_value; + const changePoint = point?.change_point; + const change = - isChange && point + isChange && point && pValue !== undefined && changePoint !== undefined ? { type, - impact: pValueToLabel(point.p_value), - time: item.occurrences[point.change_point].x, - p_value: point.p_value, + impact: pValueToLabel(pValue), + time: item.occurrences[changePoint].x, + p_value: pValue, } : undefined; From 631531341d46fb5fe253c21863130368b790453d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 9 Jun 2025 13:07:40 +0200 Subject: [PATCH 03/17] Expose feature flag to browser --- .../src/api/significant_events/index.ts | 5 +- .../kbn-streams-schema/src/queries/index.ts | 4 +- .../plugins/shared/streams/common/config.ts | 18 +-- .../plugins/shared/streams/public/plugin.ts | 39 ++++--- .../plugins/shared/streams/public/types.ts | 2 + .../streams/significant_events/route.ts | 9 +- .../public/components/asset_image/index.tsx | 2 +- .../sig_events_empty_state_dark.png | Bin 0 -> 23474 bytes .../change_point.ts | 6 +- .../change_point_summary.tsx | 2 +- .../index.tsx | 7 +- .../significant_event_flyout.tsx | 9 +- .../components/stream_detail_view/index.tsx | 16 +-- .../hooks/use_significant_events_api.ts | 103 ++++++++---------- .../public/hooks/use_stream_privileges.ts | 37 ------- .../public/hooks/use_streams_privileges.ts | 62 +++++++++++ .../public/util/hierarchy_helpers.ts | 5 +- 17 files changed, 176 insertions(+), 150 deletions(-) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/asset_image/sig_events_empty_state_dark.png delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_privileges.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/use_streams_privileges.ts diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/index.ts index 1a3aa8fddec31..ea583de640ec4 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/index.ts @@ -35,7 +35,10 @@ type SignificantEventsResponse = StreamQueryKql & { type SignificantEventsGetResponse = SignificantEventsResponse[]; -type SignificantEventsPreviewResponse = SignificantEventsResponse; +type SignificantEventsPreviewResponse = Pick< + SignificantEventsResponse, + 'occurrences' | 'change_points' | 'kql' +>; export type { SignificantEventsResponse, diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts index 93d7e44134459..4a51c625031af 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/queries/index.ts @@ -33,7 +33,7 @@ export const streamQueryKqlSchema: z.Schema = z.intersection( streamQueryBaseSchema, z.object({ kql: z.object({ - query: NonEmptyString, + query: z.string(), }), }) ); @@ -47,7 +47,7 @@ export const streamQuerySchema: z.Schema = streamQueryKqlSchema; export const upsertStreamQueryRequestSchema = z.object({ title: NonEmptyString, kql: z.object({ - query: NonEmptyString, + query: z.string(), }), }); diff --git a/x-pack/platform/plugins/shared/streams/common/config.ts b/x-pack/platform/plugins/shared/streams/common/config.ts index 8ab7d8a77adbd..00977551aeed4 100644 --- a/x-pack/platform/plugins/shared/streams/common/config.ts +++ b/x-pack/platform/plugins/shared/streams/common/config.ts @@ -25,12 +25,14 @@ export type StreamsConfig = TypeOf; * NOTE: anything exposed here will be visible in the UI dev tools, * and therefore MUST NOT be anything that is sensitive information! */ -export const exposeToBrowserConfig = {} as const; +export const exposeToBrowserConfig = { + experimental: { + significantEventsEnabled: true, + }, +} as const; -type ValidKeys = keyof { - [K in keyof typeof exposeToBrowserConfig as (typeof exposeToBrowserConfig)[K] extends true - ? K - : never]: true; -}; - -export type StreamsPublicConfig = Pick; +export interface StreamsPublicConfig { + experimental?: { + significantEventsEnabled?: boolean; + }; +} diff --git a/x-pack/platform/plugins/shared/streams/public/plugin.ts b/x-pack/platform/plugins/shared/streams/public/plugin.ts index 0e08ba519ab85..be2d6b8db2f5b 100644 --- a/x-pack/platform/plugins/shared/streams/public/plugin.ts +++ b/x-pack/platform/plugins/shared/streams/public/plugin.ts @@ -9,8 +9,9 @@ import { ApplicationStart, CoreSetup, CoreStart, PluginInitializerContext } from import { Logger } from '@kbn/logging'; import { createRepositoryClient } from '@kbn/server-route-repository-client'; -import { Observable, from, shareReplay, startWith } from 'rxjs'; +import { of, Observable, from, shareReplay, startWith } from 'rxjs'; import { once } from 'lodash'; +import { CloudStart } from '@kbn/cloud-plugin/public'; import type { StreamsPublicConfig } from '../common/config'; import { StreamsPluginClass, @@ -41,12 +42,13 @@ export class Plugin implements StreamsPluginClass { start(core: CoreStart, pluginsStart: StreamsPluginStartDependencies): StreamsPluginStart { return { streamsRepositoryClient: this.repositoryClient, - status$: createStreamsStatusObservable( - pluginsStart, - core.application, - this.repositoryClient, - this.logger - ), + status$: createStreamsStatusObservable({ + cloud: pluginsStart.cloud, + application: core.application, + repositoryClient: this.repositoryClient, + logger: this.logger, + }), + config$: of(this.config), }; } @@ -58,23 +60,28 @@ const DISABLED_STATUS: StreamsStatus = { status: 'disabled' }; const UNKNOWN_STATUS: StreamsStatus = { status: 'unknown' }; const createStreamsStatusObservable = once( - ( - deps: StreamsPluginSetupDependencies | StreamsPluginStartDependencies, - application: ApplicationStart, - repositoryClient: StreamsRepositoryClient, - logger: Logger - ): Observable => { + ({ + cloud, + application, + repositoryClient, + logger, + }: { + cloud?: CloudStart; + application: ApplicationStart; + repositoryClient: StreamsRepositoryClient; + logger: Logger; + }): Observable => { const isObservabilityServerless = - deps.cloud?.isServerlessEnabled && deps.cloud?.serverless.projectType === 'observability'; + cloud?.isServerlessEnabled && cloud.serverless.projectType === 'observability'; const hasCapabilities = application.capabilities?.streams?.show; if (!hasCapabilities) { - return from([DISABLED_STATUS]); + return of(DISABLED_STATUS); } if (isObservabilityServerless) { - return from([ENABLED_STATUS]); + return of(ENABLED_STATUS); } return from( diff --git a/x-pack/platform/plugins/shared/streams/public/types.ts b/x-pack/platform/plugins/shared/streams/public/types.ts index 2fd1fea3335ce..33b6b7ddaad79 100644 --- a/x-pack/platform/plugins/shared/streams/public/types.ts +++ b/x-pack/platform/plugins/shared/streams/public/types.ts @@ -9,6 +9,7 @@ import type { Plugin as PluginClass } from '@kbn/core/public'; import { Observable } from 'rxjs'; import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; import type { StreamsRepositoryClient } from './api'; +import { StreamsPublicConfig } from '../common/config'; export interface StreamsStatus { status: 'unknown' | 'enabled' | 'disabled'; @@ -20,6 +21,7 @@ export interface StreamsPluginSetup {} export interface StreamsPluginStart { streamsRepositoryClient: StreamsRepositoryClient; status$: Observable; + config$: Observable; } export interface StreamsPluginSetupDependencies { diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts index 999add714eafd..0bbfc87441765 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts @@ -15,7 +15,6 @@ import { SignificantEventsGetResponse, SignificantEventsPreviewResponse, StreamQueryKql, - streamQueryKqlSchema, } from '@kbn/streams-schema'; import { createTracedEsClient } from '@kbn/traced-es-client'; import { z } from '@kbn/zod'; @@ -33,7 +32,7 @@ function createSearchRequest({ }: { from: Date; to: Date; - query: StreamQueryKql; + query: Pick; bucketSize: string; }) { return { @@ -83,7 +82,11 @@ const previewSignificantEventsRoute = createServerRoute({ path: z.object({ name: z.string() }), query: z.object({ from: dateFromString, to: dateFromString, bucketSize: z.string() }), body: z.object({ - query: streamQueryKqlSchema, + query: z.object({ + kql: z.object({ + query: z.string(), + }), + }), }), }), diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/index.tsx index aa13d649afe12..328c5ecf9b6df 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/index.tsx @@ -27,7 +27,7 @@ const imageSets = { }, significantEventsEmptyState: { light: () => import('./sig_events_empty_state_light.png'), - dark: () => import('./sig_events_empty_state_light.png'), + dark: () => import('./sig_events_empty_state_dark.png'), alt: i18n.translate('xpack.streams.significantEvents.emptyStateImage', { defaultMessage: 'Empty state illustration for the Significant events view', }), diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/sig_events_empty_state_dark.png b/x-pack/platform/plugins/shared/streams_app/public/components/asset_image/sig_events_empty_state_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b782bf9b86b0111853c500f1cabe4783e1c51778 GIT binary patch literal 23474 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

Ipk0qa}0n{mXU}0bm8uZ?Bt_HskN!2d-d4X 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 literal 0 HcmV?d00001 diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts index 79a0b206fa51e..9ba53f6d574d3 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point.ts @@ -33,7 +33,7 @@ function getImpactProperties(impact: FormattedChangePoint['impact']): { if (impact === 'high') { return { color: 'danger', - label: i18n.translate('xpack.significantEventsTable.changePoint.dotImpactHigh', { + label: i18n.translate('xpack.streams.significantEventsTable.changePoint.dotImpactHigh', { defaultMessage: 'High', }), }; @@ -42,7 +42,7 @@ function getImpactProperties(impact: FormattedChangePoint['impact']): { if (impact === 'medium') { return { color: 'warning', - label: i18n.translate('xpack.significantEventsTable.changePoint.dotImpactMedium', { + label: i18n.translate('xpack.streams.significantEventsTable.changePoint.dotImpactMedium', { defaultMessage: 'Medium', }), }; @@ -50,7 +50,7 @@ function getImpactProperties(impact: FormattedChangePoint['impact']): { return { color: 'darkShade', - label: i18n.translate('xpack.significantEventsTable.changePoint.dotImpactLow', { + label: i18n.translate('xpack.streams.significantEventsTable.changePoint.dotImpactLow', { defaultMessage: 'Low', }), }; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point_summary.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point_summary.tsx index 6680a8d4081d8..8fbc13536e50a 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point_summary.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/change_point_summary.tsx @@ -29,7 +29,7 @@ export function ChangePointSummary({ white-space: nowrap; `} > - {i18n.translate('xpack.significantEventsTable.changePointBadge.noChangesDetected', { + {i18n.translate('xpack.streams.significantEventsTable.changePointBadge.noChangesDetected', { defaultMessage: 'No changes detected', })} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx index 04f26da76e558..f7a1dbe194f8f 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx @@ -26,7 +26,7 @@ import { SignificantEventsTable } from './significant_events_table'; export function StreamDetailSignificantEventsView({ definition, }: { - definition?: Streams.all.GetResponse; + definition: Streams.all.GetResponse; }) { const { core: { notifications }, @@ -47,14 +47,13 @@ export function StreamDetailSignificantEventsView({ }, [start, end]); const significantEventsFetchState = useFetchSignificantEvents({ - name: definition?.stream.name, + name: definition.stream.name, start, end, kql, }); - const { addQuery, removeQuery } = - useSignificantEventsApi({ name: definition?.stream.name }) || {}; + const { addQuery, removeQuery } = useSignificantEventsApi({ name: definition.stream.name }) || {}; const [isEditFlyoutOpen, setIsEditFlyoutOpen] = useState(false); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout.tsx index a5ba1ff47735a..d7dd53dd154f1 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout.tsx @@ -171,10 +171,7 @@ export function SignificantEventFlyoutContents({ const previewFetch = useStreamsAppFetch( ({ signal }) => { - const { id, kql, title } = queryValues; - if (!id || !kql?.query || !title) { - return; - } + const { id, kql } = queryValues; const { from, to } = getAbsoluteTimeRange(timeRange); @@ -197,9 +194,7 @@ export function SignificantEventFlyoutContents({ }, body: { query: { - id, - kql, - title, + kql: kql ?? { query: '' }, }, }, }, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx index cd69275b6568e..3eff48bb9aae3 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiBadgeGroup, EuiButton } from '@elastic/eui'; import { Streams } from '@kbn/streams-schema'; -import type { ILicense } from '@kbn/licensing-plugin/public'; import { useStreamsAppParams } from '../../hooks/use_streams_app_params'; import { StreamDetailDashboardsView } from '../stream_detail_dashboards_view'; import { StreamDetailOverview } from '../stream_detail_overview'; @@ -18,16 +17,16 @@ import { ClassicStreamBadge, LifecycleBadge } from '../stream_badges'; import { StreamsAppPageTemplate } from '../streams_app_page_template'; import { StatefulStreamsAppRouter, useStreamsAppRouter } from '../../hooks/use_streams_app_router'; import { RedirectTo } from '../redirect_to'; -import { useStreamPrivileges } from '../../hooks/use_stream_privileges'; +import { StreamsFeatures, useStreamsPrivileges } from '../../hooks/use_streams_privileges'; const getStreamDetailTabs = ({ definition, router, - license, + features, }: { definition: Streams.ingest.all.GetResponse; router: StatefulStreamsAppRouter; - license: ILicense; + features: StreamsFeatures; }) => ({ overview: { @@ -50,7 +49,7 @@ const getStreamDetailTabs = ({ defaultMessage: 'Dashboards', }), }, - ...(license.hasAtLeast('enterprise') + ...(features.significantEvents?.available ? { significant_events: { href: router.link('/{key}/{tab}', { @@ -80,7 +79,7 @@ export function StreamDetailView() { const { definition } = useStreamDetail(); - const { license } = useStreamPrivileges(); + const { features } = useStreamsPrivileges(); if (tab === 'management') { return ; @@ -90,7 +89,10 @@ export function StreamDetailView() { return ; } - const tabs = license ? getStreamDetailTabs({ definition, router, license }) : undefined; + const tabs = + features.significantEvents !== undefined + ? getStreamDetailTabs({ definition, router, features }) + : undefined; const selectedTabObject = tabs?.[tab as StreamDetailTabName]; diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_significant_events_api.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_significant_events_api.ts index f6ea1ee3f0d8d..9a0160a27013b 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_significant_events_api.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_significant_events_api.ts @@ -27,11 +27,7 @@ interface SignificantEventsApi { bulk: (operations: SignificantEventsApiBulkOperation[]) => Promise; } -export function useSignificantEventsApi({ - name, -}: { - name?: string; -}): SignificantEventsApi | undefined { +export function useSignificantEventsApi({ name }: { name: string }): SignificantEventsApi { const { dependencies: { start: { @@ -43,57 +39,52 @@ export function useSignificantEventsApi({ const { signal } = useAbortController(); return useMemo(() => { - return !name - ? undefined - : { - addQuery: async ({ kql, title, id }) => { - await streamsRepositoryClient.fetch( - 'PUT /api/streams/{name}/queries/{queryId} 2023-10-31', - { - signal, - params: { - path: { - name, - queryId: id, - }, - body: { - kql, - title, - }, - }, - } - ); - }, - removeQuery: async (id) => { - await streamsRepositoryClient.fetch( - 'DELETE /api/streams/{name}/queries/{queryId} 2023-10-31', - { - signal, - params: { - path: { - name, - queryId: id, - }, - }, - } - ); - }, - bulk: async (operations) => { - await streamsRepositoryClient.fetch( - 'POST /api/streams/{name}/queries/_bulk 2023-10-31', - { - signal, - params: { - path: { - name, - }, - body: { - operations, - }, - }, - } - ); + return { + addQuery: async ({ kql, title, id }) => { + await streamsRepositoryClient.fetch( + 'PUT /api/streams/{name}/queries/{queryId} 2023-10-31', + { + signal, + params: { + path: { + name, + queryId: id, + }, + body: { + kql, + title, + }, + }, + } + ); + }, + removeQuery: async (id) => { + await streamsRepositoryClient.fetch( + 'DELETE /api/streams/{name}/queries/{queryId} 2023-10-31', + { + signal, + params: { + path: { + name, + queryId: id, + }, + }, + } + ); + }, + bulk: async (operations) => { + await streamsRepositoryClient.fetch('POST /api/streams/{name}/queries/_bulk 2023-10-31', { + signal, + params: { + path: { + name, + }, + body: { + operations, + }, }, - }; + }); + }, + }; }, [name, signal, streamsRepositoryClient]); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_privileges.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_privileges.ts deleted file mode 100644 index 0dd8f2efcacfe..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_stream_privileges.ts +++ /dev/null @@ -1,37 +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_UI_PRIVILEGES } from '@kbn/streams-plugin/public'; -import useObservable from 'react-use/lib/useObservable'; -import type { ILicense } from '@kbn/licensing-plugin/public'; -import { useKibana } from './use_kibana'; - -export function useStreamPrivileges(): { - ui: { manage: boolean; show: boolean }; - license: ILicense | undefined; -} { - const { - core: { - application: { - capabilities: { streams }, - }, - }, - dependencies: { - start: { licensing }, - }, - } = useKibana(); - - const license = useObservable(licensing.license$); - - return { - ui: streams as { - [STREAMS_UI_PRIVILEGES.manage]: boolean; - [STREAMS_UI_PRIVILEGES.show]: boolean; - }, - license, - }; -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_streams_privileges.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_streams_privileges.ts new file mode 100644 index 0000000000000..1620e8b600f81 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_streams_privileges.ts @@ -0,0 +1,62 @@ +/* + * 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_UI_PRIVILEGES } from '@kbn/streams-plugin/public'; +import useObservable from 'react-use/lib/useObservable'; +import { useKibana } from './use_kibana'; + +export interface StreamsFeatures { + significantEvents?: { + available: boolean; + enabled: boolean; + }; +} + +export interface StreamsPrivileges { + ui: { + manage: boolean; + show: boolean; + }; + features: StreamsFeatures; +} + +export function useStreamsPrivileges(): StreamsPrivileges { + const { + core: { + application: { + capabilities: { streams }, + }, + }, + dependencies: { + start: { + licensing, + streams: { config$ }, + }, + }, + } = useKibana(); + + const license = useObservable(licensing.license$); + const streamsConfig = useObservable(config$); + + return { + ui: streams as { + [STREAMS_UI_PRIVILEGES.manage]: boolean; + [STREAMS_UI_PRIVILEGES.show]: boolean; + }, + features: { + significantEvents: + license && streamsConfig + ? { + enabled: !!streamsConfig.experimental?.significantEventsEnabled, + available: + !!streamsConfig.experimental?.significantEventsEnabled && + license.hasAtLeast('enterprise'), + } + : undefined, + }, + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts b/x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts index c9bb4024a0a5c..bb56ca973847f 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts @@ -14,9 +14,6 @@ export function getIndexPatterns(stream: Streams.all.Definition | undefined) { if (Streams.UnwiredStream.Definition.is(stream)) { return [stream.name]; } - const isRoot = stream.name.indexOf('.') === -1; const dataStreamOfDefinition = stream.name; - return isRoot - ? [dataStreamOfDefinition, `${dataStreamOfDefinition}.*`] - : [`${dataStreamOfDefinition}*`]; + return [dataStreamOfDefinition, `${dataStreamOfDefinition}.*`]; } From 44f288490a0eea651b9306a8e78e9f15019149d9 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 9 Jun 2025 16:51:06 +0200 Subject: [PATCH 04/17] Refactor --- .../preview_significant_events.ts | 120 +++++++++++ ...significant_events_from_alerts_indices.ts} | 2 +- .../streams/significant_events/route.ts | 202 ++++------------- .../public/components/spark_plot/index.tsx | 35 +-- .../empty_state.tsx | 4 +- .../index.tsx | 14 +- .../significant_event_flyout/i18n.ts | 36 ++++ .../index.tsx} | 203 ++++-------------- .../significant_events_flyout.stories.tsx | 2 +- .../significant_event_flyout/use | 0 .../use_significant_event_preview_fetch.ts | 68 ++++++ .../use_significant_event_validation.ts | 56 +++++ .../use_spark_plot_data_from_sig_events.ts | 70 ++++++ .../significant_events_histogram.tsx | 7 +- ...annotation_from_formatted_change_point.tsx | 6 +- .../uncontrolled_streams_app_bar.tsx | 3 + .../streams_chart_tooltip/index.tsx | 58 +++++ .../components/timeline/index.stories.tsx | 9 +- .../public/components/timeline/index.tsx | 58 ++--- .../hooks/use_fetch_significant_events.ts | 8 +- .../streams_app/public/routes/config.tsx | 15 +- .../shared/streams_app/public/types.ts | 2 +- 22 files changed, 540 insertions(+), 438 deletions(-) create mode 100644 x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/preview_significant_events.ts rename x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/{read_significant_events.ts => read_significant_events_from_alerts_indices.ts} (98%) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/i18n.ts rename x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/{significant_event_flyout.tsx => significant_event_flyout/index.tsx} (53%) rename x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/{ => significant_event_flyout}/significant_events_flyout.stories.tsx (92%) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_significant_event_preview_fetch.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_significant_event_validation.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_spark_plot_data_from_sig_events.ts create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/streams_chart_tooltip/index.tsx diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/preview_significant_events.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/preview_significant_events.ts new file mode 100644 index 0000000000000..5c1a10eb2f44b --- /dev/null +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/preview_significant_events.ts @@ -0,0 +1,120 @@ +/* + * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { IScopedClusterClient } from '@kbn/core/server'; +import { SignificantEventsPreviewResponse, StreamQueryKql } from '@kbn/streams-schema'; +import { InferSearchResponseOf } from '@kbn/es-types'; +import { notFound } from '@hapi/boom'; +import type { ChangePointType } from '@kbn/es-types/src'; + +type PreviewStreamQuery = Pick; + +function createSearchRequest({ + from, + to, + query, + bucketSize, +}: { + from: Date; + to: Date; + query: PreviewStreamQuery; + bucketSize: string; +}) { + return { + size: 0, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: from.toISOString(), + lte: to.toISOString(), + }, + }, + }, + { + kql: { + query: query.kql.query, + }, + // TODO: kql is not in the ES client's types yet (06-2025) + } as QueryDslQueryContainer, + ], + }, + }, + aggs: { + occurrences: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + extended_bounds: { + min: from.toISOString(), + max: to.toISOString(), + }, + }, + }, + change_points: { + change_point: { + buckets_path: 'occurrences>_count', + }, + // TODO: change_points is not in the ES client's types yet (06-2025) + } as {}, + }, + }; +} + +export async function previewSignificantEvents( + params: { + name: string; + query: PreviewStreamQuery; + from: Date; + to: Date; + bucketSize: string; + }, + dependencies: { + scopedClusterClient: IScopedClusterClient; + } +): Promise { + const { bucketSize, from, to, name, query } = params; + const { scopedClusterClient } = dependencies; + + const searchRequest = createSearchRequest({ + bucketSize, + from, + query, + to, + }); + + const response = (await scopedClusterClient.asCurrentUser.search({ + index: name, + track_total_hits: false, + ...searchRequest, + })) as InferSearchResponseOf>; + + if (!response.aggregations) { + throw notFound(); + } + + const aggregations = response.aggregations as typeof response.aggregations & { + change_points: { + type: Record; + }; + }; + + return { + ...query, + change_points: aggregations.change_points, + occurrences: + aggregations.occurrences.buckets.map((bucket) => { + return { + date: bucket.key_as_string, + count: bucket.doc_count, + }; + }) ?? [], + }; +} diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/read_significant_events.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/read_significant_events_from_alerts_indices.ts similarity index 98% rename from x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/read_significant_events.ts rename to x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/read_significant_events_from_alerts_indices.ts index 49f5cf8950b4a..0782a9b29dbd0 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/read_significant_events.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/read_significant_events_from_alerts_indices.ts @@ -16,7 +16,7 @@ import { get, isArray, isEmpty, keyBy } from 'lodash'; import { AssetClient } from '../../../lib/streams/assets/asset_client'; import { getRuleIdFromQueryLink } from '../../../lib/streams/assets/query/helpers/query'; -export async function readSignificantEvents( +export async function readSignificantEventsFromAlertsIndices( params: { name: string; from: Date; to: Date; bucketSize: string }, dependencies: { assetClient: AssetClient; diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts index 0bbfc87441765..9fcdd42b5fe00 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts @@ -5,79 +5,23 @@ * 2.0. */ -import { - AggregationsDateHistogramAggregate, - QueryDslQueryContainer, -} from '@elastic/elasticsearch/lib/api/types'; -import { badRequest, notFound } from '@hapi/boom'; -import { ChangePointType } from '@kbn/es-types/src'; +import { badRequest } from '@hapi/boom'; import { SignificantEventsGetResponse, SignificantEventsPreviewResponse, - StreamQueryKql, } from '@kbn/streams-schema'; -import { createTracedEsClient } from '@kbn/traced-es-client'; import { z } from '@kbn/zod'; -import { isEmpty } from 'lodash'; import { STREAMS_API_PRIVILEGES } from '../../../../common/constants'; import { createServerRoute } from '../../create_server_route'; +import { previewSignificantEvents } from './preview_significant_events'; +import { readSignificantEventsFromAlertsIndices } from './read_significant_events_from_alerts_indices'; -const dateFromString = z.string().pipe(z.coerce.date()); - -function createSearchRequest({ - from, - to, - query, - bucketSize, -}: { - from: Date; - to: Date; - query: Pick; - bucketSize: string; -}) { - return { - size: 0, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: from.toISOString(), - lte: to.toISOString(), - }, - }, - }, - { - kql: { - query: query.kql.query, - }, - } as QueryDslQueryContainer, - ], - }, - }, - aggs: { - occurrences: { - date_histogram: { - field: '@timestamp', - fixed_interval: bucketSize, - extended_bounds: { - min: from.toISOString(), - max: to.toISOString(), - }, - }, - }, - change_points: { - change_point: { - buckets_path: 'occurrences>_count', - }, - } as {}, - }, - }; -} +// Make sure strings are expected for input, but still converted to a +// Date, without breaking the OpenAPI generator +const dateFromString = z.string().transform((input) => new Date(input)); const previewSignificantEventsRoute = createServerRoute({ - endpoint: 'POST /api/streams/{name}/significant_events/_preview', + endpoint: 'POST /api/streams/{name}/significant_events/_preview 2023-10-31', params: z.object({ path: z.object({ name: z.string() }), query: z.object({ from: dateFromString, to: dateFromString, bucketSize: z.string() }), @@ -89,20 +33,17 @@ const previewSignificantEventsRoute = createServerRoute({ }), }), }), - options: { - access: 'internal', - summary: 'Read the significant events', - description: 'Read the significant events', + access: 'public', + summary: 'Preview significant events', + description: 'Preview significant event results based on a given query', availability: { stability: 'experimental', }, }, security: { authz: { - enabled: false, - reason: - 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', + requiredPrivileges: [STREAMS_API_PRIVILEGES.read], }, }, handler: async ({ @@ -120,50 +61,24 @@ const previewSignificantEventsRoute = createServerRoute({ throw badRequest('Streams is not enabled'); } - const tracedEsClient = createTracedEsClient({ - client: scopedClusterClient.asCurrentUser, - logger, - plugin: 'streams', - }); - - const { name } = params.path; - const { from, to, bucketSize } = params.query; - const { query } = params.body; - - const searchRequest = createSearchRequest({ - bucketSize, - from, - query, - to, - }); - - const response = await tracedEsClient.search('get_significant_event_timeseries', { - index: name, - track_total_hits: false, - ...searchRequest, - }); - - if (!response.aggregations) { - throw notFound(); - } - - const aggregations = response.aggregations as typeof response.aggregations & { - change_points: { - type: Record; - }; - }; - - return { - ...query, - change_points: aggregations.change_points, - occurrences: - aggregations.occurrences.buckets.map((bucket) => { - return { - date: bucket.key_as_string, - count: bucket.doc_count, - }; - }) ?? [], - }; + const { + body: { query }, + path: { name }, + query: { bucketSize, from, to }, + } = params; + + return await previewSignificantEvents( + { + name, + bucketSize, + from, + to, + query, + }, + { + scopedClusterClient, + } + ); }, }); @@ -204,56 +119,15 @@ const readSignificantEventsRoute = createServerRoute({ const { name } = params.path; const { from, to, bucketSize } = params.query; - const assetQueries = await assetClient.getAssetLinks(name, ['query']); - if (isEmpty(assetQueries)) { - return []; - } - - const searchRequests = assetQueries.flatMap((asset) => { - return [ - { index: name }, - createSearchRequest({ - from, - to, - bucketSize, - query: asset.query, - }), - ]; - }); - - const response = await scopedClusterClient.asCurrentUser.msearch< - unknown, - { occurrences: AggregationsDateHistogramAggregate; change_points: unknown } - >({ searches: searchRequests }); - - const significantEvents = response.responses.map((queryResponse, queryIndex) => { - const query = assetQueries[queryIndex]; - if ('error' in queryResponse) { - return { - id: query.query.id, - title: query.query.title, - kql: query.query.kql, - occurrences: [], - change_points: {}, - }; - } - - return { - id: query.query.id, - title: query.query.title, - kql: query.query.kql, - occurrences: - // @ts-ignore map unrecognized on buckets - queryResponse?.aggregations?.occurrences?.buckets.map((bucket) => ({ - date: bucket.key_as_string, - count: bucket.doc_count, - })), - change_points: queryResponse?.aggregations?.change_points, - }; - }); - - // @ts-ignore - return significantEvents; + return await readSignificantEventsFromAlertsIndices( + { + name, + from, + to, + bucketSize, + }, + { assetClient, scopedClusterClient } + ); }, }); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/spark_plot/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/spark_plot/index.tsx index 5daefcb1deb45..15440e4f1a868 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/spark_plot/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/spark_plot/index.tsx @@ -20,31 +20,10 @@ import { Tooltip, niceTimeFormatter, } from '@elastic/charts'; -import { EuiFlexGroup, EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { useKibana } from '../../hooks/use_kibana'; - -function AnnotationTooltip({ - timestamp, - label, - xFormatter, -}: { - timestamp: number; - label: React.ReactNode; - xFormatter: TickFormatter; -}) { - const formattedTime = xFormatter(timestamp); - - return ( - - - {formattedTime} - {label} - - - ); -} +import { StreamsChartTooltip } from '../streams_chart_tooltip'; export interface SparkPlotAnnotation { id: string; @@ -162,7 +141,7 @@ export function SparkPlot({ { - return ( - - ); + customTooltip={() => { + return ; }} /> ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.tsx index f7c2775fe755a..f154625f54bf6 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/empty_state.tsx @@ -15,13 +15,13 @@ export function SignificantEventsViewEmptyState({ onAddClick }: { onAddClick?: (

- {i18n.translate('xpack.significantEvents.emptyState.title', { + {i18n.translate('xpack.streams.significantEvents.emptyState.title', { defaultMessage: 'No significant event definitions', })}

- {i18n.translate('xpack.significantEvents.emptyState.description', { + {i18n.translate('xpack.streams.significantEvents.emptyState.description', { defaultMessage: 'There are no significant events defined for this stream yet.', })} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx index f7a1dbe194f8f..b9c86d9b53831 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx @@ -12,7 +12,6 @@ import React, { useMemo, useState } from 'react'; import { useFetchSignificantEvents } from '../../hooks/use_fetch_significant_events'; import { useKibana } from '../../hooks/use_kibana'; import { useSignificantEventsApi } from '../../hooks/use_significant_events_api'; -import { useStreamsAppParams } from '../../hooks/use_streams_app_params'; import { useTimefilter } from '../../hooks/use_timefilter'; import { LoadingPanel } from '../loading_panel'; import { StreamsAppSearchBar } from '../streams_app_search_bar'; @@ -32,10 +31,6 @@ export function StreamDetailSignificantEventsView({ core: { notifications }, } = useKibana(); - const { - query: { kql }, - } = useStreamsAppParams('/{key}/*'); - const { timeState: { start, end }, } = useTimefilter(); @@ -50,7 +45,6 @@ export function StreamDetailSignificantEventsView({ name: definition.stream.name, start, end, - kql, }); const { addQuery, removeQuery } = useSignificantEventsApi({ name: definition.stream.name }) || {}; @@ -74,6 +68,7 @@ export function StreamDetailSignificantEventsView({ label: , color: theme.colors[change.color], time: change.time, + header: item.query.title, }, ]; }) ?? [] @@ -169,12 +164,7 @@ export function StreamDetailSignificantEventsView({ - {}} - onQueryChange={() => {}} - /> + { - const { title = '', kql: { query: kqlQuery } = { query: '' } } = queryValues; - const titleEmptyError = title.length === 0; - const kqlEmptyError = kqlQuery.length === 0; - - const titleErrorMessage = titleEmptyError - ? i18n.translate('xpack.significantEventFlyout.formFieldTitleRequiredError', { - defaultMessage: 'Required', - }) - : undefined; - - let kqlSyntaxError = false; - - if (!kqlEmptyError) { - try { - fromKueryExpression(kqlQuery); - } catch (error) { - kqlSyntaxError = true; - } - } - - const kqlErrorMessage = kqlSyntaxError - ? i18n.translate('xpack.significantEventFlyout.formFieldQuerySyntaxError', { - defaultMessage: 'Invalid syntax', - }) - : kqlEmptyError - ? i18n.translate('xpack.significantEventFlyout.formFieldQueryRequiredError', { - defaultMessage: 'Required', - }) - : undefined; - - return { - title: titleErrorMessage, - kql: kqlErrorMessage, - }; - }, [queryValues]); + const validation = useSignificantEventValidation({ queryValues }); const validationMessages = useMemo(() => { return { @@ -169,83 +100,21 @@ export function SignificantEventFlyoutContents({ }; }, [validation, touched]); - const previewFetch = useStreamsAppFetch( - ({ signal }) => { - const { id, kql } = queryValues; - - const { from, to } = getAbsoluteTimeRange(timeRange); - - const bucketSize = calculateAuto - .near(50, moment.duration(moment(to).diff(from))) - ?.asSeconds()!; - - return streams.streamsRepositoryClient.fetch( - `POST /api/streams/{name}/significant_events/_preview`, - { - signal, - params: { - path: { - name, - }, - query: { - bucketSize: `${bucketSize}s`, - from, - to, - }, - body: { - query: { - kql: kql ?? { query: '' }, - }, - }, - }, - } - ); - }, - [timeRange, name, queryValues, streams.streamsRepositoryClient] - ); - - const sparkPlotData = useMemo(() => { - const changePoints = previewFetch.value?.change_points; - const occurrences = previewFetch.value?.occurrences; - - const timeseries = - occurrences?.map(({ date, count }) => { - return { - x: new Date(date).getTime(), - y: count, - }; - }) ?? []; - - const { id, kql, title } = queryValues; + const previewFetch = useSignificantEventPreviewFetch({ + name, + queryValues, + timeRange, + }); - const change = - changePoints && occurrences && id && kql && title - ? formatChangePoint({ - change_points: changePoints, - occurrences: timeseries, - query: { - id, - kql, - title, - }, - }) - : undefined; + const xFormatter = useMemo(() => { + return niceTimeFormatter([start, end]); + }, [start, end]); - return { - timeseries, - annotations: change - ? [ - getAnnotationFromFormattedChangePoint({ - query: { - id, - }, - change, - theme, - }), - ] - : [], - }; - }, [previewFetch.value, queryValues, theme]); + const sparkPlotData = useSparkplotDataFromSigEvents({ + previewFetch, + queryValues, + xFormatter, + }); const parsedQuery = useMemo(() => { return streamQuerySchema.safeParse(queryValues); @@ -255,7 +124,7 @@ export function SignificantEventFlyoutContents({ <> -

{getTitle(query)}

+

{getSigEventFlyoutTitle(query)}

@@ -264,7 +133,7 @@ export function SignificantEventFlyoutContents({ {...validationMessages.title} label={ - {i18n.translate('xpack.significantEventFlyout.formFieldTitleLabel', { + {i18n.translate('xpack.streams.significantEventFlyout.formFieldTitleLabel', { defaultMessage: 'Title', })} @@ -282,7 +151,7 @@ export function SignificantEventFlyoutContents({ - {i18n.translate('xpack.significantEventFlyout.formFieldQueryLabel', { + {i18n.translate('xpack.streams.significantEventFlyout.formFieldQueryLabel', { defaultMessage: 'Query', })} @@ -304,22 +173,24 @@ export function SignificantEventFlyoutContents({ }} dateRangeFrom={timeRange.from} dateRangeTo={timeRange.to} - placeholder={i18n.translate('xpack.significantEventFlyout.queryPlaceholder', { + placeholder={i18n.translate('xpack.streams.significantEventFlyout.queryPlaceholder', { defaultMessage: 'Filter events', })} dataViews={dataViewsFetch.value} + submitOnBlur /> @@ -330,7 +201,7 @@ export function SignificantEventFlyoutContents({ onClose?.(); }} > - {i18n.translate('xpack.significantEventFlyout.cancelButtonLabel', { + {i18n.translate('xpack.streams.significantEventFlyout.cancelButtonLabel', { defaultMessage: 'Cancel', })}
@@ -358,7 +229,7 @@ export function SignificantEventFlyoutContents({ } }} > - {getSubmitTitle(query)} + {getSigEventSubmitTitle(query)}
diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_flyout.stories.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/significant_events_flyout.stories.tsx similarity index 92% rename from x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_flyout.stories.tsx rename to x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/significant_events_flyout.stories.tsx index 040d263281db4..d0194f37217d7 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_flyout.stories.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/significant_events_flyout.stories.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { css } from '@emotion/css'; import classNames from 'classnames'; -import { SignificantEventFlyout } from './significant_event_flyout'; +import { SignificantEventFlyout } from '.'; const stories: Meta<{}> = { title: 'Streams/SignificantEventFlyout', diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_significant_event_preview_fetch.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_significant_event_preview_fetch.ts new file mode 100644 index 0000000000000..39edac894884d --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_significant_event_preview_fetch.ts @@ -0,0 +1,68 @@ +/* + * 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 { SignificantEventsPreviewResponse, StreamQueryKql } from '@kbn/streams-schema'; +import type { TimeRange } from '@kbn/es-query'; +import { getAbsoluteTimeRange } from '@kbn/data-plugin/common'; +import { calculateAuto } from '@kbn/calculate-auto'; +import moment from 'moment'; +import type { AbortableAsyncState } from '@kbn/react-hooks'; +import { useStreamsAppFetch } from '../../../hooks/use_streams_app_fetch'; +import { useKibana } from '../../../hooks/use_kibana'; + +export function useSignificantEventPreviewFetch({ + name, + queryValues, + timeRange, +}: { + name: string; + queryValues: Partial; + timeRange: TimeRange; +}): AbortableAsyncState> { + const { + dependencies: { + start: { streams }, + }, + } = useKibana(); + + const previewFetch = useStreamsAppFetch( + ({ signal }) => { + const { kql } = queryValues; + + const { from, to } = getAbsoluteTimeRange(timeRange); + + const bucketSize = calculateAuto + .near(50, moment.duration(moment(to).diff(from))) + ?.asSeconds()!; + + return streams.streamsRepositoryClient.fetch( + `POST /api/streams/{name}/significant_events/_preview 2023-10-31`, + { + signal, + params: { + path: { + name, + }, + query: { + bucketSize: `${bucketSize}s`, + from, + to, + }, + body: { + query: { + kql: kql ?? { query: '' }, + }, + }, + }, + } + ); + }, + [timeRange, name, queryValues, streams.streamsRepositoryClient] + ); + + return previewFetch; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_significant_event_validation.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_significant_event_validation.ts new file mode 100644 index 0000000000000..15b4a083a481b --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_significant_event_validation.ts @@ -0,0 +1,56 @@ +/* + * 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 { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { fromKueryExpression } from '@kbn/es-query'; +import type { StreamQueryKql } from '@kbn/streams-schema'; + +export function useSignificantEventValidation({ + queryValues, +}: { + queryValues: Partial; +}) { + const validation = useMemo(() => { + const { title = '', kql: { query: kqlQuery } = { query: '' } } = queryValues; + const titleEmptyError = title.length === 0; + const kqlEmptyError = kqlQuery.length === 0; + + const titleErrorMessage = titleEmptyError + ? i18n.translate('xpack.streams.significantEventFlyout.formFieldTitleRequiredError', { + defaultMessage: 'Required', + }) + : undefined; + + let kqlSyntaxError = false; + + if (!kqlEmptyError) { + try { + fromKueryExpression(kqlQuery); + } catch (error) { + kqlSyntaxError = true; + } + } + + const kqlErrorMessage = kqlSyntaxError + ? i18n.translate('xpack.streams.significantEventFlyout.formFieldQuerySyntaxError', { + defaultMessage: 'Invalid syntax', + }) + : kqlEmptyError + ? i18n.translate('xpack.streams.significantEventFlyout.formFieldQueryRequiredError', { + defaultMessage: 'Required', + }) + : undefined; + + return { + title: titleErrorMessage, + kql: kqlErrorMessage, + }; + }, [queryValues]); + + return validation; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_spark_plot_data_from_sig_events.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_spark_plot_data_from_sig_events.ts new file mode 100644 index 0000000000000..063a3ec735388 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use_spark_plot_data_from_sig_events.ts @@ -0,0 +1,70 @@ +/* + * 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 { AbortableAsyncState } from '@kbn/react-hooks'; +import { SignificantEventsPreviewResponse, StreamQueryKql } from '@kbn/streams-schema'; +import { useEuiTheme } from '@elastic/eui'; +import { TickFormatter } from '@elastic/charts'; +import { useMemo } from 'react'; +import { formatChangePoint } from '../change_point'; +import { getAnnotationFromFormattedChangePoint } from '../utils/get_annotation_from_formatted_change_point'; + +export function useSparkplotDataFromSigEvents({ + previewFetch, + queryValues, + xFormatter, +}: { + previewFetch: AbortableAsyncState>; + queryValues: Partial & { id: string }; + xFormatter: TickFormatter; +}) { + const theme = useEuiTheme().euiTheme; + + return useMemo(() => { + const changePoints = previewFetch.value?.change_points; + const occurrences = previewFetch.value?.occurrences; + + const timeseries = + occurrences?.map(({ date, count }) => { + return { + x: new Date(date).getTime(), + y: count, + }; + }) ?? []; + + const { id, kql, title } = queryValues; + + const change = + changePoints && occurrences && id && kql && title + ? formatChangePoint({ + change_points: changePoints, + occurrences: timeseries, + query: { + id, + kql, + title, + }, + }) + : undefined; + + return { + timeseries, + annotations: change + ? [ + getAnnotationFromFormattedChangePoint({ + query: { + id, + }, + change, + theme, + xFormatter, + }), + ] + : [], + }; + }, [previewFetch, xFormatter, queryValues, theme]); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_histogram.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_histogram.tsx index 08f2a7594df9b..748a50ee0b9c5 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_histogram.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_histogram.tsx @@ -17,7 +17,7 @@ interface Props { id: string; occurrences: Array<{ x: number; y: number }>; change?: FormattedChangePoint; - xFormatter?: TickFormatter; + xFormatter: TickFormatter; } export function SignificantEventsHistogramChart({ id, occurrences, change, xFormatter }: Props) { @@ -32,14 +32,15 @@ export function SignificantEventsHistogramChart({ id, occurrences, change, xForm query: { id }, change, theme, + xFormatter, }), ]; - }, [change, id, theme]); + }, [change, id, theme, xFormatter]); return ( , id: `change_point_${id}`, - label: change.label, + label: , x: change.time, }; } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx index f717dfb77daf3..47cc43305ba6b 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/streams_app_search_bar/uncontrolled_streams_app_bar.tsx @@ -21,6 +21,7 @@ export interface UncontrolledStreamsAppSearchBarProps { dataViews?: DataView[]; showSubmitButton?: boolean; showQueryInput?: boolean; + submitOnBlur?: boolean; } export function UncontrolledStreamsAppSearchBar({ @@ -34,6 +35,7 @@ export function UncontrolledStreamsAppSearchBar({ dataViews, showSubmitButton = true, showQueryInput, + submitOnBlur = false, }: UncontrolledStreamsAppSearchBarProps) { const { dependencies: { @@ -72,6 +74,7 @@ export function UncontrolledStreamsAppSearchBar({ disableQueryLanguageSwitcher placeholder={placeholder} indexPatterns={dataViews} + submitOnBlur={submitOnBlur} /> ); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/streams_chart_tooltip/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/streams_chart_tooltip/index.tsx new file mode 100644 index 0000000000000..8784704f6c14a --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/streams_chart_tooltip/index.tsx @@ -0,0 +1,58 @@ +/* + * 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 { + TooltipContainer, + TooltipTable, + TooltipTableBody, + TooltipTableCell, + TooltipTableColorCell, + TooltipTableHeader, + TooltipTableRow, +} from '@elastic/charts'; +import { useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; +import React from 'react'; + +export function StreamsChartTooltip({ + header, + label, + color, +}: { + header?: React.ReactNode; + label: React.ReactNode; + color: string; +}) { + const theme = useEuiTheme().euiTheme; + + return ( + + + + {header ? ( + + {header} + + ) : null} + + + {label} + + + + + ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.stories.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.stories.tsx index d189a29859dfb..a382af672f582 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.stories.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.stories.tsx @@ -45,19 +45,22 @@ export const WithEvents: StoryFn<{}> = () => { { id: 'some_event', time: new Date(`2025-03-24T12:30:00.000Z`).getTime(), - label: `Some event`, + header: 'Some event', + label: `Trend change`, color: theme.euiTheme.colors.danger, }, { id: 'some_other_event', time: new Date(`2025-03-24T12:10:00.000Z`).getTime(), - label: `Some other event`, + header: 'Some other event', + label: `Dip`, color: theme.euiTheme.colors.danger, }, { id: 'another_event', time: new Date(`2025-03-24T13:48:00.000Z`).getTime(), - label: `Another event`, + header: `Another event`, + label: `Spike`, color: theme.euiTheme.colors.danger, }, ]; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.tsx index ca109461a4431..474aa6d17f3a4 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/timeline/index.tsx @@ -14,12 +14,6 @@ import { Settings, TickFormatter, Tooltip, - TooltipContainer, - TooltipTable, - TooltipTableBody, - TooltipTableCell, - TooltipTableColorCell, - TooltipTableRow, } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiTitle, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/css'; @@ -29,12 +23,14 @@ import { range } from 'lodash'; import moment from 'moment'; import React, { useMemo } from 'react'; import { useKibana } from '../../hooks/use_kibana'; +import { StreamsChartTooltip } from '../streams_chart_tooltip'; export interface TimelineEvent { id: string; time: number; label: React.ReactNode; color: string; + header: React.ReactNode; } interface TimelineProps { @@ -75,23 +71,25 @@ export function Timeline({ events, start, end, xFormatter }: TimelineProps) { }, }; - const dummyValues = useMemo(() => { - if (events.length) { - return events.map((event) => { - return { - x: event.time, - }; - }); - } + // make sure there's nice ticks + const valuesWithNiceTicks = useMemo(() => { const delta = calculateAuto.atLeast(20, moment.duration(end - start))?.asMilliseconds()!; const buckets = Math.floor((end - start) / delta); - return range(0, buckets).map((index) => { - return { - x: start + index * delta, - }; - }); + const roundedStart = Math.round(start / delta) * delta; + + return range(0, buckets) + .map((index) => { + return { + x: roundedStart + index * delta, + }; + }) + .concat( + events.map((event) => { + return { x: event.time }; + }) + ); }, [start, end, events]); return ( @@ -132,7 +130,7 @@ export function Timeline({ events, start, end, xFormatter }: TimelineProps) { /> { const { event } = datum as { dataValue: number; event: TimelineEvent }; return ( - - - - - - {event.label} - - - - + ); }} /> diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_fetch_significant_events.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_fetch_significant_events.ts index 8b5a2da31cfa5..a7cc7392dd87d 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_fetch_significant_events.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_fetch_significant_events.ts @@ -21,12 +21,10 @@ export const useFetchSignificantEvents = ({ name, start, end, - kql, }: { - name?: string; + name: string; start: number; end: number; - kql?: string; }) => { const { dependencies: { @@ -39,10 +37,6 @@ export const useFetchSignificantEvents = ({ const result = useStreamsAppFetch( async ({ signal }): Promise => { - if (!name) { - return Promise.resolve(undefined); - } - const isoFrom = new Date(start).toISOString(); const isoTo = new Date(end).toISOString(); diff --git a/x-pack/platform/plugins/shared/streams_app/public/routes/config.tsx b/x-pack/platform/plugins/shared/streams_app/public/routes/config.tsx index a407a1ef175a8..f21f34e693a57 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/routes/config.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/routes/config.tsx @@ -44,18 +44,11 @@ const streamsAppRoutes = { ), - params: t.intersection([ - t.type({ - path: t.type({ - key: t.string, - }), - }), - t.partial({ - query: t.partial({ - kql: t.string, - }), + params: t.type({ + path: t.type({ + key: t.string, }), - ]), + }), children: { '/{key}': { element: , diff --git a/x-pack/platform/plugins/shared/streams_app/public/types.ts b/x-pack/platform/plugins/shared/streams_app/public/types.ts index 77e7ff628396c..f5d278cbc4d07 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/types.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/types.ts @@ -28,7 +28,7 @@ import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-p import type { SharePublicSetup, SharePublicStart } from '@kbn/share-plugin/public/plugin'; import type { StreamsPluginStart } from '@kbn/streams-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { DiscoverStart } from '@kbn/discover-plugin/public'; +import type { DiscoverStart } from '@kbn/discover-plugin/public'; /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ConfigSchema {} From e66087ab9d28a077e6bf56e7db45ede00beaef4c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:08:21 +0000 Subject: [PATCH 05/17] [CI] Auto-commit changed files from 'node scripts/capture_oas_snapshot --include-path /api/status --include-path /api/alerting/rule/ --include-path /api/alerting/rules --include-path /api/actions --include-path /api/security/role --include-path /api/spaces --include-path /api/streams --include-path /api/fleet --include-path /api/dashboards --include-path /api/saved_objects/_import --include-path /api/saved_objects/_export --include-path /api/maintenance_window --update' --- oas_docs/bundle.json | 101 +++++++++++++++++++++++++++++--- oas_docs/bundle.serverless.json | 101 +++++++++++++++++++++++++++++--- 2 files changed, 184 insertions(+), 18 deletions(-) diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 55843d89fa633..9c162bb25348c 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -49881,7 +49881,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -51993,7 +51992,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -53820,7 +53818,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -55679,7 +55676,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -57508,7 +57504,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -62170,7 +62165,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -62339,7 +62333,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -62388,7 +62381,6 @@ "name": "from", "required": true, "schema": { - "format": "date-time", "type": "string" } }, @@ -62397,7 +62389,6 @@ "name": "to", "required": true, "schema": { - "format": "date-time", "type": "string" } }, @@ -62441,6 +62432,98 @@ ], "x-state": "Technical Preview" } + }, + "/api/streams/{name}/significant_events/_preview": { + "post": { + "description": "Preview significant event results based on a given query

[Required authorization] Route required privileges: read_stream.", + "operationId": "post-streams-name-significant-events-preview", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "from", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "bucketSize", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "query": { + "additionalProperties": false, + "properties": { + "kql": { + "additionalProperties": false, + "properties": { + "query": { + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "required": [ + "kql" + ], + "type": "object" + } + }, + "required": [ + "query" + ], + "type": "object" + } + } + } + }, + "responses": {}, + "summary": "Preview significant events", + "tags": [ + "streams" + ], + "x-state": "Technical Preview" + } } }, "security": [ diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index d975dcf1d2b49..ba4bd1fb919c1 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -49472,7 +49472,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -51584,7 +51583,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -53411,7 +53409,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -55270,7 +55267,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -57099,7 +57095,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -61761,7 +61756,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -61930,7 +61924,6 @@ "additionalProperties": false, "properties": { "query": { - "minLength": 1, "type": "string" } }, @@ -61979,7 +61972,6 @@ "name": "from", "required": true, "schema": { - "format": "date-time", "type": "string" } }, @@ -61988,7 +61980,6 @@ "name": "to", "required": true, "schema": { - "format": "date-time", "type": "string" } }, @@ -62032,6 +62023,98 @@ ], "x-state": "Technical Preview" } + }, + "/api/streams/{name}/significant_events/_preview": { + "post": { + "description": "Preview significant event results based on a given query

[Required authorization] Route required privileges: read_stream.", + "operationId": "post-streams-name-significant-events-preview", + "parameters": [ + { + "description": "A required header to protect against CSRF attacks", + "in": "header", + "name": "kbn-xsrf", + "required": true, + "schema": { + "example": "true", + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "from", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "bucketSize", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": { + "query": { + "additionalProperties": false, + "properties": { + "kql": { + "additionalProperties": false, + "properties": { + "query": { + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + } + }, + "required": [ + "kql" + ], + "type": "object" + } + }, + "required": [ + "query" + ], + "type": "object" + } + } + } + }, + "responses": {}, + "summary": "Preview significant events", + "tags": [ + "streams" + ], + "x-state": "Technical Preview" + } } }, "security": [ From c9aeaefaecb5f4b6c458fc121b5df48bbca7eeaa Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:41:34 +0000 Subject: [PATCH 06/17] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 69 ++++++++++++++++++++++---- oas_docs/output/kibana.yaml | 69 ++++++++++++++++++++++---- 2 files changed, 120 insertions(+), 18 deletions(-) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index b09a878536634..8a971db37853e 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -46841,7 +46841,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -48173,7 +48172,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -49338,7 +49336,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -50523,7 +50520,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -51688,7 +51684,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -54650,7 +54645,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -54751,7 +54745,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -54780,13 +54773,11 @@ paths: name: from required: true schema: - format: date-time type: string - in: query name: to required: true schema: - format: date-time type: string - in: query name: bucketSize @@ -54810,6 +54801,66 @@ paths: tags: - streams x-state: Technical Preview + /api/streams/{name}/significant_events/_preview: + post: + description: 'Preview significant event results based on a given query

[Required authorization] Route required privileges: read_stream.' + operationId: post-streams-name-significant-events-preview + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + - in: path + name: name + required: true + schema: + type: string + - in: query + name: from + required: true + schema: + type: string + - in: query + name: to + required: true + schema: + type: string + - in: query + name: bucketSize + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + query: + additionalProperties: false + type: object + properties: + kql: + additionalProperties: false + type: object + properties: + query: + type: string + required: + - query + required: + - kql + required: + - query + responses: {} + summary: Preview significant events + tags: + - streams + x-state: Technical Preview /api/task_manager/_health: get: description: | diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index d107e7e8c1fe7..a15af8cf8965a 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -50363,7 +50363,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -51695,7 +51694,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -52860,7 +52858,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -54045,7 +54042,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -55210,7 +55206,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -58172,7 +58167,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -58273,7 +58267,6 @@ paths: type: object properties: query: - minLength: 1 type: string required: - query @@ -58302,13 +58295,11 @@ paths: name: from required: true schema: - format: date-time type: string - in: query name: to required: true schema: - format: date-time type: string - in: query name: bucketSize @@ -58332,6 +58323,66 @@ paths: tags: - streams x-state: Technical Preview + /api/streams/{name}/significant_events/_preview: + post: + description: 'Preview significant event results based on a given query

[Required authorization] Route required privileges: read_stream.' + operationId: post-streams-name-significant-events-preview + parameters: + - description: A required header to protect against CSRF attacks + in: header + name: kbn-xsrf + required: true + schema: + example: 'true' + type: string + - in: path + name: name + required: true + schema: + type: string + - in: query + name: from + required: true + schema: + type: string + - in: query + name: to + required: true + schema: + type: string + - in: query + name: bucketSize + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + additionalProperties: false + type: object + properties: + query: + additionalProperties: false + type: object + properties: + kql: + additionalProperties: false + type: object + properties: + query: + type: string + required: + - query + required: + - kql + required: + - query + responses: {} + summary: Preview significant events + tags: + - streams + x-state: Technical Preview /api/synthetics/monitors: get: description: | From 67faed670b4b675cd91c818fadf7ccf24ac6c6e3 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 10 Jun 2025 09:29:07 +0200 Subject: [PATCH 07/17] Review feedback --- .../shared/kbn-streams-schema/index.ts | 2 ++ .../src/helpers}/hierarchy_helpers.ts | 8 ++++++-- .../preview_significant_events.ts | 13 ++++++++---- .../streams/significant_events/route.ts | 4 +++- .../components/stream_chart_panel.tsx | 5 ++--- .../index.tsx | 6 +++--- .../significant_events_table.stories.tsx | 20 +++++++++++++++++-- .../significant_events_table.tsx | 7 ++++--- .../utils/discover_helpers.ts | 8 ++++++-- .../public/components/streams_list/index.tsx | 11 +++++++--- 10 files changed, 61 insertions(+), 23 deletions(-) rename x-pack/platform/{plugins/shared/streams_app/public/util => packages/shared/kbn-streams-schema/src/helpers}/hierarchy_helpers.ts (62%) 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 aaccff718b8d5..3ca73347dcc6b 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/index.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/index.ts @@ -49,6 +49,8 @@ export { type RoutingDefinition, routingDefinitionListSchema } from './src/model export { type ContentPack, contentPackSchema } from './src/content'; export { isRootStreamDefinition } from './src/helpers/is_root'; +export { getIndexPatternsForStream } from './src/helpers/hierarchy_helpers'; + export { keepFields, namespacePrefixes, diff --git a/x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/hierarchy_helpers.ts similarity index 62% rename from x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts rename to x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/hierarchy_helpers.ts index bb56ca973847f..58b7142d6a30b 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/util/hierarchy_helpers.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/helpers/hierarchy_helpers.ts @@ -5,9 +5,13 @@ * 2.0. */ -import { Streams } from '@kbn/streams-schema'; +import { Streams } from '../models/streams'; -export function getIndexPatterns(stream: Streams.all.Definition | undefined) { +export function getIndexPatternsForStream( + stream: T +): T extends Streams.all.Definition ? string[] : undefined; + +export function getIndexPatternsForStream(stream: Streams.all.Definition | undefined) { if (!stream) { return undefined; } diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/preview_significant_events.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/preview_significant_events.ts index 5c1a10eb2f44b..437beb663a232 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/preview_significant_events.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/preview_significant_events.ts @@ -7,7 +7,12 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { IScopedClusterClient } from '@kbn/core/server'; -import { SignificantEventsPreviewResponse, StreamQueryKql } from '@kbn/streams-schema'; +import { + SignificantEventsPreviewResponse, + StreamQueryKql, + Streams, + getIndexPatternsForStream, +} from '@kbn/streams-schema'; import { InferSearchResponseOf } from '@kbn/es-types'; import { notFound } from '@hapi/boom'; import type { ChangePointType } from '@kbn/es-types/src'; @@ -70,7 +75,7 @@ function createSearchRequest({ export async function previewSignificantEvents( params: { - name: string; + definition: Streams.all.Definition; query: PreviewStreamQuery; from: Date; to: Date; @@ -80,7 +85,7 @@ export async function previewSignificantEvents( scopedClusterClient: IScopedClusterClient; } ): Promise { - const { bucketSize, from, to, name, query } = params; + const { bucketSize, from, to, definition, query } = params; const { scopedClusterClient } = dependencies; const searchRequest = createSearchRequest({ @@ -91,7 +96,7 @@ export async function previewSignificantEvents( }); const response = (await scopedClusterClient.asCurrentUser.search({ - index: name, + index: getIndexPatternsForStream(definition), track_total_hits: false, ...searchRequest, })) as InferSearchResponseOf>; diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts index 9fcdd42b5fe00..3c2aebb6891be 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/significant_events/route.ts @@ -67,9 +67,11 @@ const previewSignificantEventsRoute = createServerRoute({ query: { bucketSize, from, to }, } = params; + const definition = await streamsClient.getStream(name); + return await previewSignificantEvents( { - name, + definition, bucketSize, from, to, 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 77c0db9ca3fcc..f4b995330a8e7 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,12 +15,11 @@ import { import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { Streams } from '@kbn/streams-schema'; +import { Streams, getIndexPatternsForStream } from '@kbn/streams-schema'; import { computeInterval } from '@kbn/visualization-utils'; import moment, { DurationInputArg1, DurationInputArg2 } from 'moment'; import { useKibana } from '../../../hooks/use_kibana'; import { ControlledEsqlChart } from '../../esql_chart/controlled_esql_chart'; -import { getIndexPatterns } from '../../../util/hierarchy_helpers'; import { StreamsAppSearchBar } from '../../streams_app_search_bar'; import { useStreamsAppFetch } from '../../../hooks/use_streams_app_fetch'; import { useTimefilter } from '../../../hooks/use_timefilter'; @@ -44,7 +43,7 @@ export function StreamChartPanel({ definition }: StreamChartPanelProps) { const { timeState } = useTimefilter(); const indexPatterns = useMemo(() => { - return getIndexPatterns(definition?.stream); + return getIndexPatternsForStream(definition.stream); }, [definition]); const discoverLocator = useMemo( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx index b9c86d9b53831..e4e0ba30e196b 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx @@ -140,7 +140,7 @@ export function StreamDetailSignificantEventsView({ setQueryToEdit(undefined); }} query={queryToEdit} - name={definition?.stream.name ?? ''} + name={definition.stream.name} /> ) : null; @@ -178,7 +178,7 @@ export function StreamDetailSignificantEventsView({ {i18n.translate('xpack.streams.significantEvents.addSignificantEventButton', { defaultMessage: 'Add significant event query', })} - {' '} +
@@ -189,7 +189,7 @@ export function StreamDetailSignificantEventsView({ { setIsEditFlyoutOpen(true); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.stories.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.stories.tsx index e5f98a58533b7..5b614afcd8b20 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.stories.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.stories.tsx @@ -8,6 +8,7 @@ import { Meta, StoryFn } from '@storybook/react'; import React from 'react'; import { niceTimeFormatter } from '@elastic/charts'; +import { Streams } from '@kbn/streams-schema'; import { SignificantEventsTable } from './significant_events_table'; const stories: Meta<{}> = { @@ -31,10 +32,25 @@ function generateValues() { const xFormatter = niceTimeFormatter([start, end]); +const logsStreamDefinition: Streams.WiredStream.Definition = { + name: 'logs', + description: '', + ingest: { + wired: { + fields: {}, + routing: [], + }, + lifecycle: { + inherit: {}, + }, + processing: [], + }, +}; + export const Empty: StoryFn<{}> = () => { return ( = () => { export const SomeThings: StoryFn<{}> = () => { return ( { return new Promise((resolve) => setTimeout(() => { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.tsx index 0c4d8e8569bd9..6ad93425ca9f6 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_events_table.tsx @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { AbortableAsyncState } from '@kbn/react-hooks'; import React, { useMemo, useState } from 'react'; import { TickFormatter } from '@elastic/charts'; +import { Streams } from '@kbn/streams-schema'; import { SignificantEventItem } from '../../hooks/use_fetch_significant_events'; import { useKibana } from '../../hooks/use_kibana'; import { formatChangePoint } from './change_point'; @@ -37,13 +38,13 @@ function WithLoadingSpinner({ onClick, ...props }: React.ComponentProps, 'value' | 'loading' | 'error'>; onDeleteClick?: (query: SignificantEventItem) => void; onEditClick?: (query: SignificantEventItem) => void; @@ -68,7 +69,7 @@ export function SignificantEventsTable({ render: (_, record) => ( {record.query.title} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/discover_helpers.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/discover_helpers.ts index bd18bb5ca4389..361e650866a07 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/discover_helpers.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/utils/discover_helpers.ts @@ -6,9 +6,13 @@ */ import { v4 } from 'uuid'; +import { Streams, getIndexPatternsForStream } from '@kbn/streams-schema'; import { SignificantEventItem } from '../../../hooks/use_fetch_significant_events'; -export function buildDiscoverParams(significantEvent: SignificantEventItem, name?: string) { +export function buildDiscoverParams( + significantEvent: SignificantEventItem, + definition: Streams.all.Definition +) { return { timeRange: { from: 'now-7d', @@ -20,7 +24,7 @@ export function buildDiscoverParams(significantEvent: SignificantEventItem, name }, dataViewSpec: { id: v4(), - title: name, + title: getIndexPatternsForStream(definition).join(','), timeFieldName: '@timestamp', }, }; 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 fa593e35bf8ed..29475514e43cb 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 @@ -20,11 +20,16 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { euiThemeVars } from '@kbn/ui-theme'; import { css } from '@emotion/css'; -import { Streams, getSegments, isDescendantOf, isRootStreamDefinition } from '@kbn/streams-schema'; +import { + Streams, + getIndexPatternsForStream, + getSegments, + isDescendantOf, + isRootStreamDefinition, +} from '@kbn/streams-schema'; import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; import { NestedView } from '../nested_view'; import { useKibana } from '../../hooks/use_kibana'; -import { getIndexPatterns } from '../../util/hierarchy_helpers'; export interface StreamTree { name: string; @@ -174,7 +179,7 @@ function StreamNode({ ); const discoverUrl = useMemo(() => { - const indexPatterns = getIndexPatterns(node.stream); + const indexPatterns = getIndexPatternsForStream(node.stream); if (!discoverLocator || !indexPatterns) { return undefined; From 2f7bb40bb98c85bdafc564c95bbb1567cbd8bbc4 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 12 Jun 2025 10:53:20 +0200 Subject: [PATCH 08/17] Remove threat_intelligence storybook alias again --- src/dev/storybook/aliases.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 3b3672451225a..c892fb8e2c311 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -63,7 +63,6 @@ export const storybookAliases = { serverless: 'src/platform/packages/shared/serverless/storybook/config', shared_ux: 'src/platform/packages/private/shared-ux/storybook/config', streams_app: 'x-pack/platform/plugins/shared/streams_app/.storybook', - threat_intelligence: 'x-pack/solutions/security/plugins/threat_intelligence/.storybook', triggers_actions_ui: 'x-pack/platform/plugins/shared/triggers_actions_ui/.storybook', ui_actions_enhanced: 'src/platform/plugins/shared/ui_actions_enhanced/.storybook', unified_search: 'src/platform/plugins/shared/unified_search/.storybook', From 9a3de44d63f10e8d622ddc67ab65f2a993d72d6b Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 12 Jun 2025 16:40:36 +0200 Subject: [PATCH 09/17] Remove empty file --- .../significant_event_flyout/use | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/use deleted file mode 100644 index e69de29bb2d1d..0000000000000 From 4e439b8092bdf6194bf9855daef453e1cb8a9e6e Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 13 Jun 2025 08:58:59 +0200 Subject: [PATCH 10/17] Add exposed sig events config key --- .../test/plugin_functional/test_suites/core_plugins/rendering.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts index 7b156d8345fa4..5872207a6c61c 100644 --- a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -350,6 +350,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.snapshot_restore.slm_ui.enabled (boolean?)', 'xpack.snapshot_restore.ui.enabled (boolean?)', 'xpack.stack_connectors.enableExperimental (array?)', + 'xpack.streams.experimental.significantEventsEnabled (boolean?)', 'xpack.trigger_actions_ui.enableExperimental (array?)', 'xpack.trigger_actions_ui.enableGeoTrackingThresholdAlert (boolean?)', 'xpack.trigger_actions_ui.rules.enabled (boolean?)', From 0653248b28d2f452e742db22526b822d919f29eb Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 30 Jun 2025 16:48:46 +0200 Subject: [PATCH 11/17] Use advanced setting for sig events --- .../test_suites/core_plugins/rendering.ts | 1 - .../plugins/shared/streams/common/config.ts | 13 ++----- .../plugins/shared/streams/server/plugin.ts | 1 + .../public/components/stream_badges/index.tsx | 4 +-- .../significant_event_flyout/index.tsx | 11 ++++-- .../public/hooks/use_streams_privileges.ts | 36 +++++++++++-------- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts index 5c660591de6ae..88ee8f554cc20 100644 --- a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -355,7 +355,6 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.snapshot_restore.slm_ui.enabled (boolean?)', 'xpack.snapshot_restore.ui.enabled (boolean?)', 'xpack.stack_connectors.enableExperimental (array?)', - 'xpack.streams.experimental.significantEventsEnabled (boolean?)', 'xpack.trigger_actions_ui.enableExperimental (array?)', 'xpack.trigger_actions_ui.enableGeoTrackingThresholdAlert (boolean?)', 'xpack.trigger_actions_ui.rules.enabled (boolean?)', diff --git a/x-pack/platform/plugins/shared/streams/common/config.ts b/x-pack/platform/plugins/shared/streams/common/config.ts index 6c5c2aa38ec0e..330695d438358 100644 --- a/x-pack/platform/plugins/shared/streams/common/config.ts +++ b/x-pack/platform/plugins/shared/streams/common/config.ts @@ -19,14 +19,7 @@ export type StreamsConfig = TypeOf; * NOTE: anything exposed here will be visible in the UI dev tools, * and therefore MUST NOT be anything that is sensitive information! */ -export const exposeToBrowserConfig = { - experimental: { - significantEventsEnabled: true, - }, -} as const; +export const exposeToBrowserConfig = {} as const; -export interface StreamsPublicConfig { - experimental?: { - significantEventsEnabled?: boolean; - }; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StreamsPublicConfig {} diff --git a/x-pack/platform/plugins/shared/streams/server/plugin.ts b/x-pack/platform/plugins/shared/streams/server/plugin.ts index e5e989f7e9539..ed1579a878f90 100644 --- a/x-pack/platform/plugins/shared/streams/server/plugin.ts +++ b/x-pack/platform/plugins/shared/streams/server/plugin.ts @@ -208,6 +208,7 @@ export class StreamsPlugin const isObservabilityServerless = plugins.cloud?.isServerlessEnabled && plugins.cloud?.serverless.projectType === 'observability'; + core.uiSettings.register({ [OBSERVABILITY_ENABLE_STREAMS_UI]: { category: ['observability'], 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 7c0bbf6ab5621..67f8eeeebaeb7 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 @@ -14,12 +14,12 @@ import { isErrorLifecycle, isDslLifecycle, Streams, + getIndexPatternsForStream, } from '@kbn/streams-schema'; import React from 'react'; import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { css } from '@emotion/react'; import { useKibana } from '../../hooks/use_kibana'; -import { getIndexPatterns } from '../../util/hierarchy_helpers'; const DataRetentionTooltip: React.FC<{ children: React.ReactElement }> = ({ children }) => ( (DISCOVER_APP_LOCATOR); const dataStreamExists = Streams.WiredStream.GetResponse.is(definition) || definition.data_stream_exists; - const indexPatterns = getIndexPatterns(definition.stream); + const indexPatterns = getIndexPatternsForStream(definition.stream); const esqlQuery = indexPatterns ? `FROM ${indexPatterns.join(', ')}` : undefined; if (!discoverLocator || !dataStreamExists || !esqlQuery) { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx index 8645da7e3c59a..ec1ca06e2a2d9 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx @@ -159,13 +159,18 @@ export function SignificantEventFlyoutContents({ {...validationMessages.kql} > { setTouched((prev) => ({ ...prev, kql: true })); }} onQuerySubmit={(next) => { - setQueryValues((prev) => ({ ...prev, kql: { query: next.query } })); + setQueryValues((prev) => ({ + ...prev, + kql: { + query: typeof next.query?.query === 'string' ? next.query.query : '', + }, + })); setTouched((prev) => ({ ...prev, kql: true })); if (next.dateRange) { setTimeRange(next.dateRange); @@ -176,7 +181,7 @@ export function SignificantEventFlyoutContents({ placeholder={i18n.translate('xpack.streams.significantEventFlyout.queryPlaceholder', { defaultMessage: 'Filter events', })} - dataViews={dataViewsFetch.value} + indexPatterns={dataViewsFetch.value} submitOnBlur /> diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_streams_privileges.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_streams_privileges.ts index 1620e8b600f81..7e059646404a6 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_streams_privileges.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_streams_privileges.ts @@ -7,9 +7,16 @@ import { STREAMS_UI_PRIVILEGES } from '@kbn/streams-plugin/public'; import useObservable from 'react-use/lib/useObservable'; +import { + OBSERVABILITY_ENABLE_STREAMS_UI, + OBSERVABILITY_STREAMS_ENABLE_SIGNIFICANT_EVENTS, +} from '@kbn/management-settings-ids'; import { useKibana } from './use_kibana'; export interface StreamsFeatures { + ui?: { + enabled: boolean; + }; significantEvents?: { available: boolean; enabled: boolean; @@ -30,17 +37,20 @@ export function useStreamsPrivileges(): StreamsPrivileges { application: { capabilities: { streams }, }, + uiSettings, }, dependencies: { - start: { - licensing, - streams: { config$ }, - }, + start: { licensing }, }, } = useKibana(); const license = useObservable(licensing.license$); - const streamsConfig = useObservable(config$); + + const uiEnabled = uiSettings.get(OBSERVABILITY_ENABLE_STREAMS_UI); + + const significantEventsEnabled = uiSettings.get( + OBSERVABILITY_STREAMS_ENABLE_SIGNIFICANT_EVENTS + ); return { ui: streams as { @@ -48,15 +58,13 @@ export function useStreamsPrivileges(): StreamsPrivileges { [STREAMS_UI_PRIVILEGES.show]: boolean; }, features: { - significantEvents: - license && streamsConfig - ? { - enabled: !!streamsConfig.experimental?.significantEventsEnabled, - available: - !!streamsConfig.experimental?.significantEventsEnabled && - license.hasAtLeast('enterprise'), - } - : undefined, + ui: { + enabled: uiEnabled, + }, + significantEvents: license && { + enabled: significantEventsEnabled, + available: significantEventsEnabled && license.hasAtLeast('enterprise'), + }, }, }; } From 5c1b0763dda84fe08bf99024b837096207951770 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:02:32 +0000 Subject: [PATCH 12/17] [CI] Auto-commit changed files from 'node scripts/styled_components_mapping' --- x-pack/platform/plugins/shared/streams_app/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/platform/plugins/shared/streams_app/tsconfig.json b/x-pack/platform/plugins/shared/streams_app/tsconfig.json index 047c5dcc852c3..bc9689355f708 100644 --- a/x-pack/platform/plugins/shared/streams_app/tsconfig.json +++ b/x-pack/platform/plugins/shared/streams_app/tsconfig.json @@ -72,5 +72,6 @@ "@kbn/saved-objects-tagging-plugin", "@kbn/config-schema", "@kbn/scout", + "@kbn/management-settings-ids", ] } From 2120c5e1341e5f1ec68fe9e40e4047a2a42c9dbf Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 30 Jun 2025 20:11:06 +0200 Subject: [PATCH 13/17] Move sig events tab to management view --- .../plugins/shared/streams_app/kibana.jsonc | 3 +- .../stream_detail_management/classic.tsx | 20 +++---- .../use_streams_detail_management_tabs.tsx | 52 +++++++++++++++++++ .../stream_detail_management/wired.tsx | 25 +++++---- .../index.tsx | 2 + .../components/stream_detail_view/index.tsx | 15 ------ 6 files changed, 81 insertions(+), 36 deletions(-) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/use_streams_detail_management_tabs.tsx diff --git a/x-pack/platform/plugins/shared/streams_app/kibana.jsonc b/x-pack/platform/plugins/shared/streams_app/kibana.jsonc index ce4ae89c6da25..ddc99511a4a2c 100644 --- a/x-pack/platform/plugins/shared/streams_app/kibana.jsonc +++ b/x-pack/platform/plugins/shared/streams_app/kibana.jsonc @@ -27,7 +27,6 @@ "streams", "unifiedSearch" ], - "optionalPlugins": ["observabilityAIAssistant"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "observabilityAIAssistant"] + "optionalPlugins": ["observabilityAIAssistant"], "requiredBundles": ["kibanaReact", "kibanaUtils", "observabilityAIAssistant"] } } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx index c32d0244ba353..01f2897aee722 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx @@ -10,12 +10,12 @@ import { Streams } from '@kbn/streams-schema'; import { EuiBadgeGroup, EuiCallOut, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { useStreamsAppParams } from '../../../hooks/use_streams_app_params'; import { RedirectTo } from '../../redirect_to'; -import { StreamDetailEnrichment } from '../stream_detail_enrichment'; import { ManagementTabs, Wrapper } from './wrapper'; import { StreamDetailLifecycle } from '../stream_detail_lifecycle'; import { UnmanagedElasticsearchAssets } from './unmanaged_elasticsearch_assets'; import { StreamsAppPageTemplate } from '../../streams_app_page_template'; import { ClassicStreamBadge, LifecycleBadge } from '../../stream_badges'; +import { useStreamsDetailManagementTabs } from './use_streams_detail_management_tabs'; const classicStreamManagementSubTabs = ['enrich', 'advanced', 'lifecycle'] as const; @@ -36,6 +36,11 @@ export function ClassicStreamDetailManagement({ path: { key, tab }, } = useStreamsAppParams('/{key}/management/{tab}'); + const { enrich, ...otherTabs } = useStreamsDetailManagementTabs({ + definition, + refreshDefinition, + }); + if (!definition.data_stream_exists) { return ( <> @@ -99,14 +104,7 @@ export function ClassicStreamDetailManagement({ ), }; - tabs.enrich = { - content: ( - - ), - label: i18n.translate('xpack.streams.streamDetailView.processingTab', { - defaultMessage: 'Processing', - }), - }; + tabs.enrich = enrich; } if (definition.privileges.manage) { @@ -134,6 +132,10 @@ export function ClassicStreamDetailManagement({ }; } + if (otherTabs.significantEvents) { + tabs.significantEvents = otherTabs.significantEvents; + } + if (!isValidManagementSubTab(tab)) { return ; } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/use_streams_detail_management_tabs.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/use_streams_detail_management_tabs.tsx new file mode 100644 index 0000000000000..472b15fd76377 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/use_streams_detail_management_tabs.tsx @@ -0,0 +1,52 @@ +/* + * 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 React from 'react'; +import { Streams } from '@kbn/streams-schema'; +import { i18n } from '@kbn/i18n'; +import { StreamDetailEnrichment } from '../stream_detail_enrichment'; +import { useStreamsPrivileges } from '../../../hooks/use_streams_privileges'; +import { StreamDetailSignificantEventsView } from '../../stream_detail_significant_events_view'; + +export function useStreamsDetailManagementTabs({ + definition, + refreshDefinition, +}: { + definition: Streams.ingest.all.GetResponse; + refreshDefinition: () => void; +}) { + const { + features: { significantEvents }, + } = useStreamsPrivileges(); + + const isSignificantEventsEnabled = !!significantEvents?.available; + + return { + enrich: { + content: ( + + ), + label: i18n.translate('xpack.streams.streamDetailView.processingTab', { + defaultMessage: 'Processing', + }), + }, + ...(isSignificantEventsEnabled + ? { + significantEvents: { + content: ( + + ), + label: i18n.translate('xpack.streams.streamDetailView.significantEventsTab', { + defaultMessage: 'Significant events', + }), + }, + } + : {}), + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/wired.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/wired.tsx index e61d55a683ba9..7dfd53109fa7c 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/wired.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/wired.tsx @@ -11,12 +11,18 @@ import { EuiToolTip } from '@elastic/eui'; import { useStreamsAppParams } from '../../../hooks/use_streams_app_params'; import { RedirectTo } from '../../redirect_to'; import { StreamDetailRouting } from '../stream_detail_routing'; -import { StreamDetailEnrichment } from '../stream_detail_enrichment'; import { StreamDetailSchemaEditor } from '../stream_detail_schema_editor'; import { StreamDetailLifecycle } from '../stream_detail_lifecycle'; import { Wrapper } from './wrapper'; +import { useStreamsDetailManagementTabs } from './use_streams_detail_management_tabs'; -const wiredStreamManagementSubTabs = ['route', 'enrich', 'schemaEditor', 'lifecycle'] as const; +const wiredStreamManagementSubTabs = [ + 'route', + 'enrich', + 'schemaEditor', + 'lifecycle', + 'significantEvents', +] as const; type WiredStreamManagementSubTab = (typeof wiredStreamManagementSubTabs)[number]; @@ -35,6 +41,11 @@ export function WiredStreamDetailManagement({ path: { key, tab }, } = useStreamsAppParams('/{key}/management/{tab}'); + const { enrich, ...otherTabs } = useStreamsDetailManagementTabs({ + definition, + refreshDefinition, + }); + const tabs = { lifecycle: { content: ( @@ -64,14 +75,7 @@ export function WiredStreamDetailManagement({ defaultMessage: 'Partitioning', }), }, - enrich: { - content: ( - - ), - label: i18n.translate('xpack.streams.streamDetailView.processingTab', { - defaultMessage: 'Processing', - }), - }, + enrich, schemaEditor: { content: ( @@ -80,6 +84,7 @@ export function WiredStreamDetailManagement({ defaultMessage: 'Schema editor', }), }, + ...otherTabs, }; if (!isValidManagementSubTab(tab)) { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx index e4e0ba30e196b..69166391ec0da 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/index.tsx @@ -24,8 +24,10 @@ import { SignificantEventsTable } from './significant_events_table'; export function StreamDetailSignificantEventsView({ definition, + refreshDefinition, }: { definition: Streams.all.GetResponse; + refreshDefinition: () => void; }) { const { core: { notifications }, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx index acaf895121f46..bcb694d4b4315 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx @@ -16,7 +16,6 @@ import { RedirectTo } from '../redirect_to'; import { ClassicStreamBadge, LifecycleBadge } from '../stream_badges'; import { StreamDetailDashboardsView } from '../stream_detail_dashboards_view'; import { StreamDetailOverview } from '../stream_detail_overview'; -import { StreamDetailSignificantEventsView } from '../stream_detail_significant_events_view'; import { StreamsAppPageTemplate } from '../streams_app_page_template'; import { StreamDescription } from './description'; @@ -50,20 +49,6 @@ const getStreamDetailTabs = ({ defaultMessage: 'Dashboards', }), }, - ...(features.significantEvents?.available - ? { - significant_events: { - href: router.link('/{key}/{tab}', { - path: { key: definition.stream.name, tab: 'significant_events' }, - }), - content: , - label: i18n.translate('xpack.streams.streamDetailView.significantEventsTab', { - defaultMessage: 'Significant events', - }), - background: true, - }, - } - : {}), } as const); export type StreamDetailTabs = ReturnType; From fc7fbeabc05fcb7daa805508efd644387559e0b5 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 1 Jul 2025 07:52:28 +0200 Subject: [PATCH 14/17] Allowlist significantEvents tab in classic --- .../data_management/stream_detail_management/classic.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx index 01f2897aee722..bf6668dc58cdb 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx @@ -17,7 +17,12 @@ import { StreamsAppPageTemplate } from '../../streams_app_page_template'; import { ClassicStreamBadge, LifecycleBadge } from '../../stream_badges'; import { useStreamsDetailManagementTabs } from './use_streams_detail_management_tabs'; -const classicStreamManagementSubTabs = ['enrich', 'advanced', 'lifecycle'] as const; +const classicStreamManagementSubTabs = [ + 'enrich', + 'advanced', + 'lifecycle', + 'significantEvents', +] as const; type ClassicStreamManagementSubTab = (typeof classicStreamManagementSubTabs)[number]; From 75de44c65f3cc3bfa4a1274386c03872dc9b3328 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 3 Jul 2025 14:59:48 +0200 Subject: [PATCH 15/17] Fix formatting --- x-pack/platform/plugins/shared/streams_app/kibana.jsonc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/streams_app/kibana.jsonc b/x-pack/platform/plugins/shared/streams_app/kibana.jsonc index ddc99511a4a2c..ce4ae89c6da25 100644 --- a/x-pack/platform/plugins/shared/streams_app/kibana.jsonc +++ b/x-pack/platform/plugins/shared/streams_app/kibana.jsonc @@ -27,6 +27,7 @@ "streams", "unifiedSearch" ], - "optionalPlugins": ["observabilityAIAssistant"], "requiredBundles": ["kibanaReact", "kibanaUtils", "observabilityAIAssistant"] + "optionalPlugins": ["observabilityAIAssistant"], + "requiredBundles": ["kibanaReact", "kibanaUtils", "observabilityAIAssistant"] } } From f0485aeda6c82a94efb99158b112c8ab2d1d0586 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Thu, 3 Jul 2025 10:48:54 -0400 Subject: [PATCH 16/17] Fix query input not displayed on first render --- .../significant_event_flyout/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx index ec1ca06e2a2d9..9bbed476ec38f 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx @@ -159,7 +159,11 @@ export function SignificantEventFlyoutContents({ {...validationMessages.kql} > { setTouched((prev) => ({ ...prev, kql: true })); From 64cb201f1fb7e5a8c6b6ec18db4c2b0c1f51d3dc Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 7 Jul 2025 10:25:40 +0200 Subject: [PATCH 17/17] Make sure page doesn't crash while license etc is loading --- .../data_management/stream_detail_management/wrapper.tsx | 2 +- .../significant_event_flyout/index.tsx | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/wrapper.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/wrapper.tsx index 9bc9f1d85c662..c1a6b53b542bb 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/wrapper.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/wrapper.tsx @@ -91,7 +91,7 @@ export function Wrapper({ isSelected: tab === tabKey, }))} /> - {tabs[tab].content} + {tabs[tab]?.content} ); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx index ec1ca06e2a2d9..4b8f65e0dce5d 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/significant_event_flyout/index.tsx @@ -50,6 +50,9 @@ export function SignificantEventFlyoutContents({ }: SignificantEventFlyoutProps) { const [queryValues, setQueryValues] = useState<{ id: string } & Partial>({ id: v4(), + kql: { + query: '', + }, ...query, });