From bb1ff8db33e8023813b4e917e7059f769d6873ab Mon Sep 17 00:00:00 2001 From: Achyut Jhunjhunwala Date: Tue, 31 Mar 2026 12:16:40 +0200 Subject: [PATCH 1/8] Add callout with working promote action. Review is still WIP --- .../index.tsx | 3 + .../promotion_callout.tsx | 158 ++++++++++++++++++ .../sig_events/use_promotable_queries.ts | 32 ++++ 3 files changed, 193 insertions(+) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx index 9b978fdfdade0..b89730ee23e05 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx @@ -39,6 +39,7 @@ import { KnowledgeIndicatorsStatusFilter } from './knowledge_indicators_status_f import { KnowledgeIndicatorsTypeFilter } from './knowledge_indicators_type_filter'; import { RulesTable } from './rules_table'; import { LoadingPanel } from '../../loading_panel'; +import { PromotionCallout } from './promotion_callout'; const SEARCH_DEBOUNCE_MS = 300; @@ -173,6 +174,8 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { return ( <> + {}} /> + void; +} + +export function PromotionCallout({ streamName, onReviewClick }: PromotionCalloutProps) { + const { euiTheme } = useEuiTheme(); + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); + const queryClient = useQueryClient(); + + const { count, queryIds, refetch } = usePromotableQueries(streamName); + const { promote } = useQueriesApi(); + + const promoteMutation = useMutation<{ promoted: number }, Error>({ + mutationFn: () => promote({ queryIds }), + onSuccess: async ({ promoted }) => { + toasts.addSuccess( + i18n.translate('xpack.streams.significantEvents.promotionCallout.successToast', { + defaultMessage: + '{count, plural, one {# query} other {# queries}} promoted to {count, plural, one {rule} other {rules}} successfully.', + values: { count: promoted }, + }) + ); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: DISCOVERY_QUERIES_QUERY_KEY }), + queryClient.invalidateQueries({ queryKey: UNBACKED_QUERIES_COUNT_QUERY_KEY }), + ]); + refetch(); + }, + onError: (error) => { + toasts.addError(getFormattedError(error), { + title: i18n.translate('xpack.streams.significantEvents.promotionCallout.errorToast', { + defaultMessage: 'Failed to promote queries', + }), + }); + }, + }); + + if (count === 0) { + return null; + } + + return ( + + + + + + + + +

+ + {i18n.translate( + 'xpack.streams.significantEvents.promotionCallout.queryCount', + { + defaultMessage: '{count, plural, one {# query} other {# queries}}', + values: { count }, + } + )} + + ), + ruleCount: i18n.translate( + 'xpack.streams.significantEvents.promotionCallout.ruleCount', + { + defaultMessage: '{count, plural, one {# rule} other {# rules}}', + values: { count }, + } + ), + }} + /> +

+
+
+ + + + + promoteMutation.mutate()} + isLoading={promoteMutation.isLoading} + data-test-subj="streamsAppPromotionCalloutPromoteButton" + > + {i18n.translate('xpack.streams.significantEvents.promotionCallout.promoteButton', { + defaultMessage: 'Promote', + })} + + + + + {i18n.translate('xpack.streams.significantEvents.promotionCallout.reviewButton', { + defaultMessage: 'Review results', + })} + + + + +
+
+ ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts new file mode 100644 index 0000000000000..e320d330d03fe --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts @@ -0,0 +1,32 @@ +/* + * 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 { useFetchDiscoveryQueries } from './use_fetch_discovery_queries'; + +/** + * Returns the count and IDs of promotable (draft/not-yet-promoted) queries for a specific stream. + * + * perPage is set high (1000) to ensure we always capture all draft queries in a single request. + * The API returns `total` regardless of page size, so the count is always accurate. + * IDs are used by the promote action — a hard cap would silently leave queries un-promoted. + * In practice a stream will never have anywhere near 1000 simultaneous draft queries. + */ +export function usePromotableQueries(streamName: string) { + const result = useFetchDiscoveryQueries({ + name: streamName, + status: ['draft'], + page: 1, + perPage: 1_000, + }); + + return { + count: result.data?.total ?? 0, + queryIds: result.data?.queries.map((q) => q.query.id) ?? [], + isLoading: result.isLoading, + refetch: result.refetch, + }; +} From 2ad1713db0520f029d1536bcf4ea588cb8e40318 Mon Sep 17 00:00:00 2001 From: Achyut Jhunjhunwala Date: Tue, 31 Mar 2026 15:07:40 +0200 Subject: [PATCH 2/8] Add flyout for review results section --- .../index.tsx | 16 +- .../promotion_callout.tsx | 2 +- .../suggested_rules_flyout.tsx | 277 ++++++++++++++++++ .../sig_events/use_promotable_queries.ts | 1 + 4 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout.tsx diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx index b89730ee23e05..9de00de4c715c 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx @@ -40,6 +40,7 @@ import { KnowledgeIndicatorsTypeFilter } from './knowledge_indicators_type_filte import { RulesTable } from './rules_table'; import { LoadingPanel } from '../../loading_panel'; import { PromotionCallout } from './promotion_callout'; +import { SuggestedRulesFlyout } from './suggested_rules_flyout'; const SEARCH_DEBOUNCE_MS = 300; @@ -145,6 +146,7 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { ), [knowledgeIndicators] ); + const [isSuggestedRulesFlyoutOpen, setIsSuggestedRulesFlyoutOpen] = useState(false); const isRulesSelected = useMemo( () => typeFilterOptions.some((option) => option.key === 'rule' && option.checked === 'on'), @@ -174,7 +176,12 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { return ( <> - {}} /> + + setIsSuggestedRulesFlyoutOpen(true)} + /> + @@ -266,6 +273,13 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { onClose={() => setSelectedKnowledgeIndicator(null)} /> ) : null} + + {isSuggestedRulesFlyoutOpen && ( + setIsSuggestedRulesFlyoutOpen(false)} + /> + )} ); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout.tsx index ce2fc47262d63..2bfa0795577de 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout.tsx @@ -81,7 +81,7 @@ export function PromotionCallout({ streamName, onReviewClick }: PromotionCallout hasBorder={false} data-test-subj="streamsAppPromotionCallout" > - + diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout.tsx new file mode 100644 index 0000000000000..1bb518758ecac --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout.tsx @@ -0,0 +1,277 @@ +/* + * 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, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiText, + EuiTitle, + useGeneratedHtmlId, + type CriteriaWithPagination, + type EuiBasicTableColumn, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useMutation, useQueryClient } from '@kbn/react-query'; +import React, { useCallback, useState } from 'react'; + +const PAGE_SIZE_OPTIONS = [10, 20, 50] as const; +import { + DISCOVERY_QUERIES_QUERY_KEY, + type SignificantEventQueryRow, +} from '../../../hooks/sig_events/use_fetch_discovery_queries'; +import { UNBACKED_QUERIES_COUNT_QUERY_KEY } from '../../../hooks/sig_events/use_unbacked_queries_count'; +import { usePromotableQueries } from '../../../hooks/sig_events/use_promotable_queries'; +import { useQueriesApi } from '../../../hooks/sig_events/use_queries_api'; +import { getFormattedError } from '../../../util/errors'; +import { useKibana } from '../../../hooks/use_kibana'; +import { SeverityBadge } from '../significant_events_discovery/components/severity_badge/severity_badge'; + +interface SuggestedRulesFlyoutProps { + streamName: string; + onClose: () => void; +} + +export function SuggestedRulesFlyout({ streamName, onClose }: SuggestedRulesFlyoutProps) { + const flyoutTitleId = useGeneratedHtmlId({ prefix: 'suggestedRulesFlyout' }); + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); + const queryClient = useQueryClient(); + + const { count, queries, queryIds, refetch, isLoading } = usePromotableQueries(streamName); + + const [pagination, setPagination] = useState({ index: 0, size: 20 }); + + const onTableChange = useCallback( + ({ page }: CriteriaWithPagination) => { + if (!page) return; + setPagination(page); + }, + [] + ); + + const pageOfQueries = queries.slice( + pagination.index * pagination.size, + (pagination.index + 1) * pagination.size + ); + const { promote, removeQuery } = useQueriesApi(); + + const invalidateQueriesData = async () => + Promise.all([ + queryClient.invalidateQueries({ queryKey: DISCOVERY_QUERIES_QUERY_KEY }), + queryClient.invalidateQueries({ queryKey: UNBACKED_QUERIES_COUNT_QUERY_KEY }), + ]); + + const createRulesMutation = useMutation<{ promoted: number }, Error>({ + mutationFn: () => promote({ queryIds }), + onSuccess: async ({ promoted }) => { + toasts.addSuccess( + i18n.translate('xpack.streams.suggestedRulesFlyout.createRulesSuccess', { + defaultMessage: '{count, plural, one {# rule} other {# rules}} created successfully.', + values: { count: promoted }, + }) + ); + await invalidateQueriesData(); + onClose(); + }, + onError: (error) => { + toasts.addError(getFormattedError(error), { + title: i18n.translate('xpack.streams.suggestedRulesFlyout.createRulesError', { + defaultMessage: 'Failed to create rules', + }), + }); + }, + }); + + const deleteQueryMutation = useMutation({ + mutationFn: (item) => removeQuery({ queryId: item.query.id, streamName: item.stream_name }), + onSuccess: async () => { + await invalidateQueriesData(); + refetch(); + }, + onError: (error) => { + toasts.addError(getFormattedError(error), { + title: i18n.translate('xpack.streams.suggestedRulesFlyout.deleteQueryError', { + defaultMessage: 'Failed to delete query', + }), + }); + }, + }); + + const columns: Array> = [ + { + field: 'expand', + name: '', + width: '40px', + render: () => ( + + ), + }, + { + field: 'query.title', + name: i18n.translate('xpack.streams.suggestedRulesFlyout.rulesColumn', { + defaultMessage: 'Rules', + }), + truncateText: true, + render: (_: unknown, item: SignificantEventQueryRow) => ( + + {item.query.title} + + ), + }, + { + field: 'query.severity_score', + name: i18n.translate('xpack.streams.suggestedRulesFlyout.severityColumn', { + defaultMessage: 'Severity', + }), + width: '120px', + render: (_: unknown, item: SignificantEventQueryRow) => ( + + ), + }, + { + name: '', + width: '40px', + actions: [ + { + name: i18n.translate('xpack.streams.suggestedRulesFlyout.deleteAction', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.streams.suggestedRulesFlyout.deleteActionDescription', + { defaultMessage: 'Remove this suggested rule' } + ), + icon: 'trash', + type: 'icon', + color: 'danger', + onClick: (item: SignificantEventQueryRow) => deleteQueryMutation.mutate(item), + 'data-test-subj': 'suggestedRulesFlyoutDeleteButton', + }, + ], + }, + ]; + + return ( + + + + + +

+ {i18n.translate('xpack.streams.suggestedRulesFlyout.title', { + defaultMessage: 'Suggested rules', + })} +

+
+
+ + +

+ {i18n.translate('xpack.streams.suggestedRulesFlyout.description', { + defaultMessage: + 'We generate rules based on the queries that are of critical importance for this stream. You can review and discard results.', + })} +

+
+
+
+
+ + + + + + {i18n.translate('xpack.streams.suggestedRulesFlyout.showing', { + defaultMessage: 'Showing {count, plural, one {# Rule} other {# Rules}}', + values: { count }, + })} + + + + item.query.id} + items={pageOfQueries} + loading={isLoading || deleteQueryMutation.isLoading} + noItemsMessage={ + !isLoading + ? i18n.translate('xpack.streams.suggestedRulesFlyout.noItems', { + defaultMessage: 'No suggested rules found.', + }) + : '' + } + pagination={{ + pageIndex: pagination.index, + pageSize: pagination.size, + totalItemCount: count, + pageSizeOptions: [...PAGE_SIZE_OPTIONS], + }} + onChange={onTableChange} + data-test-subj="suggestedRulesTable" + /> + + + + + + + + + {i18n.translate('xpack.streams.suggestedRulesFlyout.cancelButton', { + defaultMessage: 'Cancel', + })} + + + + createRulesMutation.mutate()} + isLoading={createRulesMutation.isLoading} + isDisabled={queryIds.length === 0} + data-test-subj="suggestedRulesFlyoutCreateButton" + > + {i18n.translate('xpack.streams.suggestedRulesFlyout.createRulesButton', { + defaultMessage: 'Create rules', + })} + + + + +
+ ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts index e320d330d03fe..de8f7b17e45e6 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts @@ -25,6 +25,7 @@ export function usePromotableQueries(streamName: string) { return { count: result.data?.total ?? 0, + queries: result.data?.queries ?? [], queryIds: result.data?.queries.map((q) => q.query.id) ?? [], isLoading: result.isLoading, refetch: result.refetch, From 5fa10fa8ff38f0b12b3a9540d8a96d6eb5b88af3 Mon Sep 17 00:00:00 2001 From: Achyut Jhunjhunwala Date: Tue, 31 Mar 2026 15:32:10 +0200 Subject: [PATCH 3/8] Update decision comment with doc link --- .../public/hooks/sig_events/use_promotable_queries.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts index de8f7b17e45e6..a9e5acffc8cff 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_promotable_queries.ts @@ -11,6 +11,7 @@ import { useFetchDiscoveryQueries } from './use_fetch_discovery_queries'; * Returns the count and IDs of promotable (draft/not-yet-promoted) queries for a specific stream. * * perPage is set high (1000) to ensure we always capture all draft queries in a single request. + * The decision to set 1_000 can be found here - https://github.com/elastic/streams-program/blob/e7a11a8a3414a9f581d504b0840ff5d93f6e8564/docs/significant-events/design-decisions/2026-03-31-promote-query-default-count.md * The API returns `total` regardless of page size, so the count is always accurate. * IDs are used by the promote action — a hard cap would silently leave queries un-promoted. * In practice a stream will never have anywhere near 1000 simultaneous draft queries. From 025f6b4330d5850463b0fdfede8502bef55d27a9 Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Wed, 1 Apr 2026 11:11:25 +0200 Subject: [PATCH 4/8] Follow ups --- .../change_point_summary.tsx | 109 ----- .../feature_identification_control.tsx | 398 ------------------ .../index.tsx | 13 +- .../promotion_callout.tsx | 14 +- .../severity_selector/index.ts | 8 - .../severity_selector/severity_selector.tsx | 63 --- .../significant_events_histogram.tsx | 69 --- .../suggested_rules_flyout.tsx | 16 +- .../timeline/index.tsx | 172 -------- .../utils/change_point.ts | 90 ---- ...annotation_from_formatted_change_point.tsx | 34 -- .../utils/p_value_to_label.ts | 19 - 12 files changed, 24 insertions(+), 981 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/change_point_summary.tsx delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/feature_identification_control.tsx rename x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/{ => promotion_callout}/promotion_callout.tsx (90%) delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/index.ts delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/severity_selector.tsx delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/significant_events_histogram.tsx rename x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/{ => suggested_rules_flyout}/suggested_rules_flyout.tsx (93%) delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/timeline/index.tsx delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/change_point.ts delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/get_annotation_from_formatted_change_point.tsx delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/p_value_to_label.ts diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/change_point_summary.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/change_point_summary.tsx deleted file mode 100644 index 0cfcace8e0f58..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/change_point_summary.tsx +++ /dev/null @@ -1,109 +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 { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiIcon, - EuiText, - useEuiTheme, -} from '@elastic/eui'; -import { css } from '@emotion/css'; -import React from 'react'; -import type { TickFormatter } from '@elastic/charts'; -import { i18n } from '@kbn/i18n'; -import type { FormattedChangePoint } from './utils/change_point'; - -const MAX_VISIBLE_CHANGES = 5; - -export function ChangePointSummary({ - changes, - xFormatter, -}: { - changes: FormattedChangePoint[]; - xFormatter: TickFormatter; -}) { - const visibleChanges = changes.slice(0, MAX_VISIBLE_CHANGES); - - return ( - - {visibleChanges.map((change, index) => ( - <> - - {index < changes.length - 1 && } - - ))} - {changes.length > MAX_VISIBLE_CHANGES && ( - - - {i18n.translate('xpack.streams.changePointSummary.moreChanges', { - defaultMessage: '+ {count} more', - values: { count: changes.length - MAX_VISIBLE_CHANGES }, - })} - - - )} - - ); -} - -function SummaryItem({ - change, - xFormatter, -}: { - change: FormattedChangePoint; - xFormatter: TickFormatter; -}) { - const theme = useEuiTheme().euiTheme; - return ( - <> - - - - - - - - - - {change.label} - - - - - {change.type} - - - - - - @ {xFormatter(change.time)} - - - - - - - - {i18n.translate('xpack.streams.changePointSummary.significantEvent', { - defaultMessage: 'Significant event:', - })} - {' '} - {change.query.title} - - - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/feature_identification_control.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/feature_identification_control.tsx deleted file mode 100644 index 993a57003dcb3..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/feature_identification_control.tsx +++ /dev/null @@ -1,398 +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 React, { useCallback, useEffect, useRef } from 'react'; -import useAsyncFn from 'react-use/lib/useAsyncFn'; -import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useBoolean } from '@kbn/react-hooks'; -import { i18n } from '@kbn/i18n'; -import { TaskStatus, type Streams } from '@kbn/streams-schema'; -import type { AIFeatures } from '../../../hooks/use_ai_features'; -import { useStreamFeaturesApi } from '../../../hooks/sig_events/use_stream_features_api'; -import { useTaskPolling } from '../../../hooks/use_task_polling'; - -interface FeatureIdentificationControlProps { - definition: Streams.all.Definition; - refreshFeatures: () => void; - aiFeatures: AIFeatures | null; - isIdentifyingFeatures: boolean; - onTaskStart: () => void; - onTaskEnd: () => void; -} - -export function FeatureIdentificationControl({ - definition, - refreshFeatures, - aiFeatures, - isIdentifyingFeatures, - onTaskStart, - onTaskEnd, -}: FeatureIdentificationControlProps) { - const { - getFeaturesIdentificationStatus, - scheduleFeaturesIdentificationTask, - cancelFeaturesIdentificationTask, - } = useStreamFeaturesApi(definition); - - const [{ loading: isGettingTask, value: task, error }, getTask] = useAsyncFn( - getFeaturesIdentificationStatus - ); - const previousStatusRef = useRef(); - const [isNoResultsDismissed, { on: dismissNoResults, off: resetNoResultsDismissed }] = - useBoolean(false); - - useEffect(() => { - getTask(); - }, [getTask]); - - const { cancelTask, isCancellingTask } = useTaskPolling({ - task, - onPoll: getFeaturesIdentificationStatus, - onRefresh: getTask, - onCancel: cancelFeaturesIdentificationTask, - }); - - // Refresh features periodically while the task is running - useEffect(() => { - if (task?.status !== TaskStatus.InProgress) { - return; - } - const interval = setInterval(refreshFeatures, 10_000); - return () => clearInterval(interval); - }, [task?.status, refreshFeatures]); - - // Sync task status with parent component - only trigger on status changes - useEffect(() => { - const currentStatus = task?.status; - const previousStatus = previousStatusRef.current; - - // Skip if status hasn't changed - if (currentStatus === previousStatus) { - return; - } - - previousStatusRef.current = currentStatus; - - if (currentStatus === TaskStatus.InProgress) { - resetNoResultsDismissed(); - onTaskStart(); - } else if (currentStatus === TaskStatus.Completed) { - refreshFeatures(); - onTaskEnd(); - } else if (currentStatus !== undefined) { - onTaskEnd(); - } - }, [task?.status, refreshFeatures, onTaskStart, onTaskEnd, resetNoResultsDismissed]); - - const handleStartIdentification = useCallback(() => { - onTaskStart(); - scheduleFeaturesIdentificationTask().then(getTask); - }, [onTaskStart, scheduleFeaturesIdentificationTask, getTask]); - - const handleCancelIdentification = useCallback(() => { - cancelTask().then(onTaskEnd); - }, [cancelTask, onTaskEnd]); - - if (error) { - return ; - } - - if (task === undefined) { - return null; - } - - const isLoading = isIdentifyingFeatures || isGettingTask; - - switch (task.status) { - case TaskStatus.NotStarted: - case TaskStatus.Acknowledged: - case TaskStatus.Canceled: - return ( - - ); - - case TaskStatus.Completed: - return ( - - ); - - case TaskStatus.InProgress: - return isCancellingTask ? ( - - ) : ( - - ); - - case TaskStatus.BeingCanceled: - return ; - - case TaskStatus.Failed: - return ( - - {task.error} - - ); - - case TaskStatus.Stale: - return ( - - {TASK_STALE_DESCRIPTION} - - ); - } - - assertNever(task); -} - -function assertNever(value: never): never { - throw new Error(`Unhandled task status: ${JSON.stringify(value)}`); -} - -// Sub-components - -const COMMON_BUTTON_PROPS = { - size: 'm', - iconType: 'sparkles', - 'data-test-subj': 'feature_identification_identify_features_button', -} as const; - -interface TriggerButtonProps { - isLoading: boolean; - onClick: () => void; - aiFeatures: AIFeatures | null; -} - -function TriggerButton({ isLoading, onClick, aiFeatures }: TriggerButtonProps) { - return ( - - {IDENTIFY_FEATURES_BUTTON_LABEL} - - ); -} - -interface CompletedStateProps { - isLoading: boolean; - onStartIdentification: () => void; - showNoResultsCallout: boolean; - onDismissNoResults: () => void; - aiFeatures: AIFeatures | null; -} - -function CompletedState({ - isLoading, - onStartIdentification, - showNoResultsCallout, - onDismissNoResults, - aiFeatures, -}: CompletedStateProps) { - if (showNoResultsCallout) { - return ( - - - - - - - {NO_FEATURES_IDENTIFIED_DESCRIPTION} - - - - ); - } - - return ( - - ); -} - -interface InProgressStateProps { - onCancel: () => void; -} - -function InProgressState({ onCancel }: InProgressStateProps) { - return ( - - - - {IN_PROGRESS_BUTTON_LABEL} - - - - - {CANCEL_BUTTON_LABEL} - - - - ); -} - -function CancellingState() { - return ( - - {CANCELLING_BUTTON_LABEL} - - ); -} - -interface StateWithCalloutProps { - isLoading: boolean; - onStartIdentification: () => void; - calloutTitle: string; - calloutColor: 'danger' | 'warning' | 'primary'; - calloutIcon: 'error' | 'warning' | 'search'; - children: React.ReactNode; - aiFeatures: AIFeatures | null; -} - -function StateWithCallout({ - isLoading, - onStartIdentification, - calloutTitle, - calloutColor, - calloutIcon, - children, - aiFeatures, -}: StateWithCalloutProps) { - return ( - - - - - - - {children} - - - - ); -} - -interface LoadingErrorCalloutProps { - errorMessage: string; -} - -function LoadingErrorCallout({ errorMessage }: LoadingErrorCalloutProps) { - return ( - - {errorMessage} - - ); -} - -// i18n labels - -const IDENTIFY_FEATURES_BUTTON_LABEL = i18n.translate( - 'xpack.streams.streamDetailView.featureIdentificationButtonLabel', - { defaultMessage: 'Identify features' } -); - -const IN_PROGRESS_BUTTON_LABEL = i18n.translate( - 'xpack.streams.streamDetailView.featureIdentificationButtonInProgressLabel', - { defaultMessage: 'Feature identification in progress' } -); - -const CANCEL_BUTTON_LABEL = i18n.translate( - 'xpack.streams.streamDetailView.cancelFeatureIdentificationButtonLabel', - { defaultMessage: 'Cancel' } -); - -const CANCELLING_BUTTON_LABEL = i18n.translate( - 'xpack.streams.streamDetailView.featureIdentificationButtonCancellingLabel', - { defaultMessage: 'Canceling feature identification task' } -); - -const LOADING_TASK_FAILED_TITLE = i18n.translate( - 'xpack.streams.streamDetailView.featureIdentificationLoadingTaskFailedLabel', - { defaultMessage: 'Failed to load feature identification task status' } -); - -const TASK_FAILED_TITLE = i18n.translate( - 'xpack.streams.streamDetailView.featureIdentificationTaskFailedLabel', - { defaultMessage: 'Feature identification task failed' } -); - -const TASK_STALE_TITLE = i18n.translate( - 'xpack.streams.streamDetailView.featureIdentificationTaskStaledLabel', - { defaultMessage: 'Feature identification task did not complete' } -); - -const TASK_STALE_DESCRIPTION = i18n.translate( - 'xpack.streams.streamDetailView.featureIdentificationTaskStaledDescription', - { - defaultMessage: - "The feature identification task didn't report its status for a prolonged period and is considered stale. Please start a new task.", - } -); - -const NO_FEATURES_IDENTIFIED_TITLE = i18n.translate( - 'xpack.streams.streamDetailView.noFeaturesIdentifiedTitle', - { defaultMessage: 'No features identified' } -); - -const NO_FEATURES_IDENTIFIED_DESCRIPTION = i18n.translate( - 'xpack.streams.streamDetailView.noFeaturesIdentifiedDescription', - { - defaultMessage: - "The feature identification task didn't find any new features in your data. You can try again with different AI connector settings or try later with new data ingested.", - } -); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx index 9de00de4c715c..c638e8ff2d1a5 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx @@ -39,8 +39,8 @@ import { KnowledgeIndicatorsStatusFilter } from './knowledge_indicators_status_f import { KnowledgeIndicatorsTypeFilter } from './knowledge_indicators_type_filter'; import { RulesTable } from './rules_table'; import { LoadingPanel } from '../../loading_panel'; -import { PromotionCallout } from './promotion_callout'; -import { SuggestedRulesFlyout } from './suggested_rules_flyout'; +import { PromotionCallout } from './promotion_callout/promotion_callout'; +import { SuggestedRulesFlyout } from './suggested_rules_flyout/suggested_rules_flyout'; const SEARCH_DEBOUNCE_MS = 300; @@ -88,7 +88,12 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { completedTaskState: Extract, { status: TaskStatus.Completed }> ) => { const queriesTaskResult = completedTaskState.queriesTaskResult; - const generatedKnowledgeIndicatorsCount = + const featuresTaskResult = completedTaskState.featuresTaskResult; + const generatedFeaturesCount = + featuresTaskResult?.status === TaskStatus.Completed + ? featuresTaskResult.features.length + : 0; + const generatedQueriesCount = queriesTaskResult?.status === TaskStatus.Completed ? queriesTaskResult.queries.length : 0; toasts.addSuccess({ @@ -98,7 +103,7 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { defaultMessage: '{count, plural, one {Generated # knowledge indicator} other {Generated # knowledge indicators}}', values: { - count: generatedKnowledgeIndicatorsCount, + count: generatedFeaturesCount + generatedQueriesCount, }, } ), diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx similarity index 90% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout.tsx rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx index 2bfa0795577de..375687d120e9c 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx @@ -19,13 +19,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useMutation, useQueryClient } from '@kbn/react-query'; import React from 'react'; -import { DISCOVERY_QUERIES_QUERY_KEY } from '../../../hooks/sig_events/use_fetch_discovery_queries'; -import { UNBACKED_QUERIES_COUNT_QUERY_KEY } from '../../../hooks/sig_events/use_unbacked_queries_count'; -import { useQueriesApi } from '../../../hooks/sig_events/use_queries_api'; -import { usePromotableQueries } from '../../../hooks/sig_events/use_promotable_queries'; -import { getFormattedError } from '../../../util/errors'; -import { useKibana } from '../../../hooks/use_kibana'; -import { AssetImage } from '../../asset_image'; +import { DISCOVERY_QUERIES_QUERY_KEY } from '../../../../hooks/sig_events/use_fetch_discovery_queries'; +import { UNBACKED_QUERIES_COUNT_QUERY_KEY } from '../../../../hooks/sig_events/use_unbacked_queries_count'; +import { useQueriesApi } from '../../../../hooks/sig_events/use_queries_api'; +import { usePromotableQueries } from '../../../../hooks/sig_events/use_promotable_queries'; +import { getFormattedError } from '../../../../util/errors'; +import { useKibana } from '../../../../hooks/use_kibana'; +import { AssetImage } from '../../../asset_image'; interface PromotionCalloutProps { streamName: string; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/index.ts deleted file mode 100644 index 523c51d0612d1..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { SeveritySelector } from './severity_selector'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/severity_selector.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/severity_selector.tsx deleted file mode 100644 index 680fd259b7d3a..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/severity_selector.tsx +++ /dev/null @@ -1,63 +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 React from 'react'; -import { EuiSuperSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - SeverityBadge, - SIGNIFICANT_EVENT_SEVERITY, - scoreSeverity, -} from '../severity_badge/severity_badge'; - -export function SeveritySelector({ - severityScore, - onChange, - disabled, -}: { - severityScore: number | undefined; - onChange: (score: number | undefined) => void; - disabled?: boolean; -}) { - const severityOptions = [ - { - value: -1, - inputDisplay: , - }, - ...Object.values(SIGNIFICANT_EVENT_SEVERITY).map((severity) => ({ - value: severity.defaultValue, - inputDisplay: , - })), - ].reverse(); - - return ( - onChange(value === -1 ? undefined : value)} - placeholder={i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.severityPlaceholder', - { defaultMessage: 'Select severity' } - )} - fullWidth - /> - ); -} - -const SEVERITY_SELECTOR_ARIA_LABEL = i18n.translate( - 'xpack.streams.significantEvents.severitySelector.ariaLabel', - { - defaultMessage: 'Select severity', - } -); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/significant_events_histogram.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/significant_events_histogram.tsx deleted file mode 100644 index 6f8e9af797435..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/significant_events_histogram.tsx +++ /dev/null @@ -1,69 +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 { groupBy } from 'lodash'; -import { useEuiTheme } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import type { TickFormatter } from '@elastic/charts'; -import type { SparkPlotAnnotation } from '../../spark_plot'; -import { SparkPlot } from '../../spark_plot'; -import type { FormattedChangePoint } from './utils/change_point'; -import { getAnnotationFromFormattedChangePoint } from './utils/get_annotation_from_formatted_change_point'; - -interface Props { - id: string; - occurrences: Array<{ x: number; y: number }>; - changes: FormattedChangePoint[]; - xFormatter: TickFormatter; - height?: number; - compressed?: boolean; - maxYValue?: number; -} - -export function SignificantEventsHistogramChart({ - id, - occurrences, - changes, - xFormatter, - compressed = true, - height, - maxYValue, -}: Props) { - const theme = useEuiTheme().euiTheme; - - const annotations = useMemo((): SparkPlotAnnotation[] => { - if (!changes.length) { - return []; - } - - return Object.entries(groupBy(changes, 'time')).map(([time, groupedByTimestamp]) => - getAnnotationFromFormattedChangePoint({ - time: Number(time), - changes: groupedByTimestamp, - theme, - xFormatter, - }) - ); - }, [changes, theme, xFormatter]); - - return ( - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx similarity index 93% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout.tsx rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx index 1bb518758ecac..ae4b21baf1721 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx @@ -30,13 +30,13 @@ const PAGE_SIZE_OPTIONS = [10, 20, 50] as const; import { DISCOVERY_QUERIES_QUERY_KEY, type SignificantEventQueryRow, -} from '../../../hooks/sig_events/use_fetch_discovery_queries'; -import { UNBACKED_QUERIES_COUNT_QUERY_KEY } from '../../../hooks/sig_events/use_unbacked_queries_count'; -import { usePromotableQueries } from '../../../hooks/sig_events/use_promotable_queries'; -import { useQueriesApi } from '../../../hooks/sig_events/use_queries_api'; -import { getFormattedError } from '../../../util/errors'; -import { useKibana } from '../../../hooks/use_kibana'; -import { SeverityBadge } from '../significant_events_discovery/components/severity_badge/severity_badge'; +} from '../../../../hooks/sig_events/use_fetch_discovery_queries'; +import { UNBACKED_QUERIES_COUNT_QUERY_KEY } from '../../../../hooks/sig_events/use_unbacked_queries_count'; +import { usePromotableQueries } from '../../../../hooks/sig_events/use_promotable_queries'; +import { useQueriesApi } from '../../../../hooks/sig_events/use_queries_api'; +import { getFormattedError } from '../../../../util/errors'; +import { useKibana } from '../../../../hooks/use_kibana'; +import { SeverityBadge } from '../../significant_events_discovery/components/severity_badge/severity_badge'; interface SuggestedRulesFlyoutProps { streamName: string; @@ -175,7 +175,7 @@ export function SuggestedRulesFlyout({ streamName, onClose }: SuggestedRulesFlyo { - const delta = calculateAuto.atLeast(20, moment.duration(end - start))?.asMilliseconds()!; - - const buckets = Math.floor((end - start) / 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 ( - - - - -

- {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 ( - - ); - }} - /> - -
-
- ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/change_point.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/change_point.ts deleted file mode 100644 index d8c009b59c67b..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/change_point.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiThemeComputed } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import type { StreamQuery } from '@kbn/streams-schema'; -import type { $Values } from 'utility-types'; -import type { SignificantEventItem } from '../../../../hooks/sig_events/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 { - query: StreamQuery; - 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.streams.significantEventsTable.changePoint.dotImpactHigh', { - defaultMessage: 'High', - }), - }; - } - - if (impact === 'medium') { - return { - color: 'warning', - label: i18n.translate('xpack.streams.significantEventsTable.changePoint.dotImpactMedium', { - defaultMessage: 'Medium', - }), - }; - } - - return { - color: 'darkShade', - label: i18n.translate('xpack.streams.significantEventsTable.changePoint.dotImpactLow', { - defaultMessage: 'Low', - }), - }; -} - -export function formatChangePoint( - item: Omit -): 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 pValue = point?.p_value; - const changePoint = point?.change_point; - - const change = - isChange && point && pValue !== undefined && changePoint !== undefined - ? { - type, - impact: pValueToLabel(pValue), - time: item.occurrences[changePoint].x, - p_value: pValue, - } - : undefined; - - return change - ? { - ...change, - ...getImpactProperties(change.impact), - query: item.query, - } - : undefined; -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/get_annotation_from_formatted_change_point.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/get_annotation_from_formatted_change_point.tsx deleted file mode 100644 index 3e882bb140821..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/get_annotation_from_formatted_change_point.tsx +++ /dev/null @@ -1,34 +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 React from 'react'; -import type { EuiThemeComputed } from '@elastic/eui'; -import { EuiIcon } from '@elastic/eui'; -import type { TickFormatter } from '@elastic/charts'; -import type { FormattedChangePoint } from './change_point'; -import { ChangePointSummary } from '../change_point_summary'; - -export function getAnnotationFromFormattedChangePoint({ - theme, - time, - changes, - xFormatter, -}: { - theme: EuiThemeComputed; - time: number; - changes: FormattedChangePoint[]; - xFormatter: TickFormatter; -}) { - const change = changes[0]; - const color = theme.colors[change.color]; - return { - color, - icon: , - id: `change_point_${time}`, - label: , - x: time, - }; -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/p_value_to_label.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/p_value_to_label.ts deleted file mode 100644 index 3f6e0836d129b..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/p_value_to_label.ts +++ /dev/null @@ -1,19 +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. - */ - -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'; - } -} From 74ae8662d276352784d289afcec9e6aca1702a4a Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Wed, 1 Apr 2026 11:31:44 +0200 Subject: [PATCH 5/8] Address comments From 00935ec41e7366a7ca286e97b64604830efcf8d4 Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Wed, 1 Apr 2026 13:03:37 +0200 Subject: [PATCH 6/8] Add child flyout with query details --- .../index.tsx | 25 +- .../knowledge_indicators_table.tsx | 41 +- .../promotion_callout/promotion_callout.tsx | 2 +- .../rules_table/rules_table.tsx | 30 +- .../suggested_rules_flyout.tsx | 407 +++++++++++------- 5 files changed, 326 insertions(+), 179 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx index c638e8ff2d1a5..2a973d3ddae1d 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx @@ -153,6 +153,25 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { ); const [isSuggestedRulesFlyoutOpen, setIsSuggestedRulesFlyoutOpen] = useState(false); + const toggleSelectedKnowledgeIndicator = useCallback((knowledgeIndicator: KnowledgeIndicator) => { + setSelectedKnowledgeIndicator((currentKnowledgeIndicator) => { + if (!currentKnowledgeIndicator) { + return knowledgeIndicator; + } + + const currentId = + currentKnowledgeIndicator.kind === 'feature' + ? currentKnowledgeIndicator.feature.uuid + : currentKnowledgeIndicator.query.id; + const nextId = + knowledgeIndicator.kind === 'feature' + ? knowledgeIndicator.feature.uuid + : knowledgeIndicator.query.id; + + return currentId === nextId ? null : knowledgeIndicator; + }); + }, []); + const isRulesSelected = useMemo( () => typeFilterOptions.some((option) => option.key === 'rule' && option.checked === 'on'), [typeFilterOptions] @@ -255,7 +274,8 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { rules={ruleKnowledgeIndicators} occurrencesByQueryId={occurrencesByQueryId} searchTerm={debouncedTableSearchValue} - onViewDetails={setSelectedKnowledgeIndicator} + selectedKnowledgeIndicator={selectedKnowledgeIndicator} + onViewDetails={toggleSelectedKnowledgeIndicator} /> ) : ( )}
diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx index fe73378f8ea9d..123d52d5f8085 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx @@ -15,6 +15,7 @@ import { EuiFlexItem, EuiHorizontalRule, EuiInMemoryTable, + EuiLink, EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -35,6 +36,7 @@ interface KnowledgeIndicatorsTableProps { searchTerm: string; selectedTypes: string[]; statusFilter: 'active' | 'excluded'; + selectedKnowledgeIndicator: KnowledgeIndicator | null; onViewDetails: (knowledgeIndicator: KnowledgeIndicator) => void; } @@ -53,6 +55,7 @@ export function KnowledgeIndicatorsTable({ searchTerm, selectedTypes, statusFilter, + selectedKnowledgeIndicator, onViewDetails, }: KnowledgeIndicatorsTableProps) { const [selectedKnowledgeIndicators, setSelectedKnowledgeIndicators] = useState< @@ -142,34 +145,25 @@ export function KnowledgeIndicatorsTable({ ? knowledgeIndicator.feature.title ?? knowledgeIndicator.feature.id : knowledgeIndicator.query.title ?? knowledgeIndicator.query.id; - if (knowledgeIndicator.kind === 'feature') { - return ( - - - onViewDetails(knowledgeIndicator)} - /> - - - {title} - - - ); - } + const isExpanded = + (knowledgeIndicator.kind === 'feature' && + selectedKnowledgeIndicator?.kind === 'feature' && + selectedKnowledgeIndicator.feature.uuid === knowledgeIndicator.feature.uuid) || + (knowledgeIndicator.kind === 'query' && + selectedKnowledgeIndicator?.kind === 'query' && + selectedKnowledgeIndicator.query.id === knowledgeIndicator.query.id); return ( onViewDetails(knowledgeIndicator)} /> - {title} + onViewDetails(knowledgeIndicator)}>{title} ); @@ -236,7 +230,7 @@ export function KnowledgeIndicatorsTable({ ), }, ], - [definition, occurrencesByQueryId, onViewDetails] + [definition, occurrencesByQueryId, onViewDetails, selectedKnowledgeIndicator] ); return ( @@ -387,6 +381,13 @@ const VIEW_DETAILS_ARIA_LABEL = i18n.translate( } ); +const MINIMIZE_DETAILS_ARIA_LABEL = i18n.translate( + 'xpack.streams.knowledgeIndicatorsTable.minimizeDetailsAriaLabel', + { + defaultMessage: 'Collapse details', + } +); + const DELETE_KNOWLEDGE_INDICATORS_MODAL_TITLE = (count: number) => i18n.translate('xpack.streams.deleteKnowledgeIndicatorsModal.title', { defaultMessage: diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx index 375687d120e9c..d82de2ba5646f 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx @@ -83,7 +83,7 @@ export function PromotionCallout({ streamName, onReviewClick }: PromotionCallout > - + diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx index 515637da1309b..8c961375a21ac 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiHorizontalRule, EuiInMemoryTable, + EuiLink, EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -33,6 +34,7 @@ interface RulesTableProps { rules: KnowledgeIndicator[]; occurrencesByQueryId: Record>; searchTerm: string; + selectedKnowledgeIndicator: KnowledgeIndicator | null; onViewDetails: (knowledgeIndicator: KnowledgeIndicator) => void; } @@ -41,6 +43,7 @@ export function RulesTable({ rules, occurrencesByQueryId, searchTerm, + selectedKnowledgeIndicator, onViewDetails, }: RulesTableProps) { const [selectedRules, setSelectedRules] = useState([]); @@ -110,13 +113,25 @@ export function RulesTable({ onViewDetails(item)} /> - {item.query.title || item.query.id} + onViewDetails(item)}> + {item.query.title || item.query.id} + ); @@ -187,7 +202,7 @@ export function RulesTable({ ), }, ], - [isDeleting, occurrencesByQueryId, onViewDetails] + [isDeleting, occurrencesByQueryId, onViewDetails, selectedKnowledgeIndicator] ); return ( @@ -340,3 +355,10 @@ const DELETE_RULES_MODAL_TITLE = (count: number) => const VIEW_DETAILS_ARIA_LABEL = i18n.translate('xpack.streams.rulesTable.viewDetailsAriaLabel', { defaultMessage: 'View details', }); + +const MINIMIZE_DETAILS_ARIA_LABEL = i18n.translate( + 'xpack.streams.rulesTable.minimizeDetailsAriaLabel', + { + defaultMessage: 'Collapse details', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx index ae4b21baf1721..84d563a523e53 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx @@ -6,7 +6,7 @@ */ import { - EuiBasicTable, + type CriteriaWithPagination, EuiButton, EuiButtonEmpty, EuiButtonIcon, @@ -16,17 +16,16 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, + EuiInMemoryTable, + EuiLink, EuiText, EuiTitle, useGeneratedHtmlId, - type CriteriaWithPagination, type EuiBasicTableColumn, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useMutation, useQueryClient } from '@kbn/react-query'; -import React, { useCallback, useState } from 'react'; - -const PAGE_SIZE_OPTIONS = [10, 20, 50] as const; +import React, { useEffect, useState } from 'react'; import { DISCOVERY_QUERIES_QUERY_KEY, type SignificantEventQueryRow, @@ -36,8 +35,11 @@ import { usePromotableQueries } from '../../../../hooks/sig_events/use_promotabl import { useQueriesApi } from '../../../../hooks/sig_events/use_queries_api'; import { getFormattedError } from '../../../../util/errors'; import { useKibana } from '../../../../hooks/use_kibana'; +import { KnowledgeIndicatorQueryDetailsContent } from '../knowledge_indicator_details_flyout/knowledge_indicator_query_details_content'; import { SeverityBadge } from '../../significant_events_discovery/components/severity_badge/severity_badge'; +const PAGE_SIZE_OPTIONS = [10, 20, 50] as const; + interface SuggestedRulesFlyoutProps { streamName: string; onClose: () => void; @@ -45,6 +47,7 @@ interface SuggestedRulesFlyoutProps { export function SuggestedRulesFlyout({ streamName, onClose }: SuggestedRulesFlyoutProps) { const flyoutTitleId = useGeneratedHtmlId({ prefix: 'suggestedRulesFlyout' }); + const detailsFlyoutTitleId = useGeneratedHtmlId({ prefix: 'suggestedRulesDetailsFlyout' }); const { core: { notifications: { toasts }, @@ -53,22 +56,33 @@ export function SuggestedRulesFlyout({ streamName, onClose }: SuggestedRulesFlyo const queryClient = useQueryClient(); const { count, queries, queryIds, refetch, isLoading } = usePromotableQueries(streamName); + const [selectedQueryRow, setSelectedQueryRow] = useState(null); + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 }); + const { promote, removeQuery } = useQueriesApi(); - const [pagination, setPagination] = useState({ index: 0, size: 20 }); + const toggleDetailsFlyout = (item: SignificantEventQueryRow) => { + setSelectedQueryRow((previous) => (previous?.query.id === item.query.id ? null : item)); + }; - const onTableChange = useCallback( - ({ page }: CriteriaWithPagination) => { - if (!page) return; - setPagination(page); - }, - [] - ); + useEffect(() => { + /** + * Ensuring stable pagination when queries get + * deleted from the table + */ + setPagination((currentPagination) => { + const pageCount = Math.ceil(queries.length / currentPagination.pageSize); + const maxPageIndex = Math.max(pageCount - 1, 0); - const pageOfQueries = queries.slice( - pagination.index * pagination.size, - (pagination.index + 1) * pagination.size - ); - const { promote, removeQuery } = useQueriesApi(); + if (currentPagination.pageIndex <= maxPageIndex) { + return currentPagination; + } + + return { + ...currentPagination, + pageIndex: maxPageIndex, + }; + }); + }, [queries.length]); const invalidateQueriesData = async () => Promise.all([ @@ -79,20 +93,13 @@ export function SuggestedRulesFlyout({ streamName, onClose }: SuggestedRulesFlyo const createRulesMutation = useMutation<{ promoted: number }, Error>({ mutationFn: () => promote({ queryIds }), onSuccess: async ({ promoted }) => { - toasts.addSuccess( - i18n.translate('xpack.streams.suggestedRulesFlyout.createRulesSuccess', { - defaultMessage: '{count, plural, one {# rule} other {# rules}} created successfully.', - values: { count: promoted }, - }) - ); + toasts.addSuccess(CREATE_RULES_SUCCESS_MESSAGE(promoted)); await invalidateQueriesData(); onClose(); }, onError: (error) => { toasts.addError(getFormattedError(error), { - title: i18n.translate('xpack.streams.suggestedRulesFlyout.createRulesError', { - defaultMessage: 'Failed to create rules', - }), + title: CREATE_RULES_ERROR_TITLE, }); }, }); @@ -105,9 +112,7 @@ export function SuggestedRulesFlyout({ streamName, onClose }: SuggestedRulesFlyo }, onError: (error) => { toasts.addError(getFormattedError(error), { - title: i18n.translate('xpack.streams.suggestedRulesFlyout.deleteQueryError', { - defaultMessage: 'Failed to delete query', - }), + title: DELETE_QUERY_ERROR_TITLE, }); }, }); @@ -117,33 +122,35 @@ export function SuggestedRulesFlyout({ streamName, onClose }: SuggestedRulesFlyo field: 'expand', name: '', width: '40px', - render: () => ( + render: (_: unknown, item: SignificantEventQueryRow) => ( toggleDetailsFlyout(item)} + data-test-subj="suggestedRulesFlyoutExpandButton" /> ), }, { field: 'query.title', - name: i18n.translate('xpack.streams.suggestedRulesFlyout.rulesColumn', { - defaultMessage: 'Rules', - }), + name: RULES_COLUMN_LABEL, truncateText: true, render: (_: unknown, item: SignificantEventQueryRow) => ( - {item.query.title} + toggleDetailsFlyout(item)} + data-test-subj="suggestedRulesFlyoutQueryTitleLink" + > + {item.query.title} + ), }, { field: 'query.severity_score', - name: i18n.translate('xpack.streams.suggestedRulesFlyout.severityColumn', { - defaultMessage: 'Severity', - }), + name: SEVERITY_COLUMN_LABEL, width: '120px', render: (_: unknown, item: SignificantEventQueryRow) => ( @@ -154,17 +161,17 @@ export function SuggestedRulesFlyout({ streamName, onClose }: SuggestedRulesFlyo width: '40px', actions: [ { - name: i18n.translate('xpack.streams.suggestedRulesFlyout.deleteAction', { - defaultMessage: 'Delete', - }), - description: i18n.translate( - 'xpack.streams.suggestedRulesFlyout.deleteActionDescription', - { defaultMessage: 'Remove this suggested rule' } - ), + name: DELETE_ACTION_LABEL, + description: DELETE_ACTION_DESCRIPTION, icon: 'trash', type: 'icon', color: 'danger', - onClick: (item: SignificantEventQueryRow) => deleteQueryMutation.mutate(item), + onClick: (item: SignificantEventQueryRow) => { + if (selectedQueryRow?.query.id === item.query.id) { + setSelectedQueryRow(null); + } + deleteQueryMutation.mutate(item); + }, 'data-test-subj': 'suggestedRulesFlyoutDeleteButton', }, ], @@ -172,106 +179,202 @@ export function SuggestedRulesFlyout({ streamName, onClose }: SuggestedRulesFlyo ]; return ( - - - - - -

- {i18n.translate('xpack.streams.suggestedRulesFlyout.title', { - defaultMessage: 'Suggested rules', - })} -

-
-
- - -

- {i18n.translate('xpack.streams.suggestedRulesFlyout.description', { - defaultMessage: - 'We generate rules based on the queries that are of critical importance for this stream. You can review and discard results.', - })} -

-
-
-
-
- - - - - - {i18n.translate('xpack.streams.suggestedRulesFlyout.showing', { - defaultMessage: 'Showing {count, plural, one {# Rule} other {# Rules}}', - values: { count }, - })} - - - - item.query.id} - items={pageOfQueries} - loading={isLoading || deleteQueryMutation.isLoading} - noItemsMessage={ - !isLoading - ? i18n.translate('xpack.streams.suggestedRulesFlyout.noItems', { - defaultMessage: 'No suggested rules found.', - }) - : '' - } - pagination={{ - pageIndex: pagination.index, - pageSize: pagination.size, - totalItemCount: count, - pageSizeOptions: [...PAGE_SIZE_OPTIONS], - }} - onChange={onTableChange} - data-test-subj="suggestedRulesTable" - /> - - - - - - - - - {i18n.translate('xpack.streams.suggestedRulesFlyout.cancelButton', { - defaultMessage: 'Cancel', - })} - - - - createRulesMutation.mutate()} - isLoading={createRulesMutation.isLoading} - isDisabled={queryIds.length === 0} - data-test-subj="suggestedRulesFlyoutCreateButton" - > - {i18n.translate('xpack.streams.suggestedRulesFlyout.createRulesButton', { - defaultMessage: 'Create rules', - })} - - - - -
+ <> + + + + + +

{FLYOUT_TITLE}

+
+
+ + +

{FLYOUT_DESCRIPTION}

+
+
+
+
+ + + + + + {SHOWING_RULES_LABEL(count)} + + + + + tableCaption={TABLE_CAPTION} + columns={columns} + itemId={(item) => item.query.id} + items={queries} + loading={isLoading || deleteQueryMutation.isLoading} + noItemsMessage={!isLoading ? NO_ITEMS_MESSAGE : ''} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageSizeOptions: [...PAGE_SIZE_OPTIONS], + }} + onTableChange={({ page }: CriteriaWithPagination) => { + if (!page) { + return; + } + + setPagination({ + pageIndex: page.index, + pageSize: page.size, + }); + }} + data-test-subj="suggestedRulesTable" + /> + + + + + {selectedQueryRow ? ( + setSelectedQueryRow(null)} + aria-labelledby={detailsFlyoutTitleId} + ownFocus={false} + session="inherit" + data-test-subj="suggestedRulesFlyoutDetailsFlyout" + > + + +

{selectedQueryRow.query.title}

+
+
+ + + +
+ ) : null} + + + + + + {CANCEL_BUTTON_LABEL} + + + + createRulesMutation.mutate()} + isLoading={createRulesMutation.isLoading} + isDisabled={queryIds.length === 0} + data-test-subj="suggestedRulesFlyoutCreateButton" + > + {CREATE_RULES_BUTTON_LABEL} + + + + +
+ ); } + +const CREATE_RULES_SUCCESS_MESSAGE = (count: number) => + i18n.translate('xpack.streams.suggestedRulesFlyout.createRulesSuccess', { + defaultMessage: '{count, plural, one {# rule} other {# rules}} created successfully.', + values: { count }, + }); + +const CREATE_RULES_ERROR_TITLE = i18n.translate( + 'xpack.streams.suggestedRulesFlyout.createRulesError', + { + defaultMessage: 'Failed to create rules', + } +); + +const DELETE_QUERY_ERROR_TITLE = i18n.translate( + 'xpack.streams.suggestedRulesFlyout.deleteQueryError', + { + defaultMessage: 'Failed to delete query', + } +); + +const MINIMIZE_ARIA_LABEL = i18n.translate('xpack.streams.suggestedRulesFlyout.minimizeAriaLabel', { + defaultMessage: 'Collapse row details', +}); + +const EXPAND_ARIA_LABEL = i18n.translate('xpack.streams.suggestedRulesFlyout.expandAriaLabel', { + defaultMessage: 'Expand row details', +}); + +const RULES_COLUMN_LABEL = i18n.translate('xpack.streams.suggestedRulesFlyout.rulesColumn', { + defaultMessage: 'Rules', +}); + +const SEVERITY_COLUMN_LABEL = i18n.translate('xpack.streams.suggestedRulesFlyout.severityColumn', { + defaultMessage: 'Severity', +}); + +const DELETE_ACTION_LABEL = i18n.translate('xpack.streams.suggestedRulesFlyout.deleteAction', { + defaultMessage: 'Delete', +}); + +const DELETE_ACTION_DESCRIPTION = i18n.translate( + 'xpack.streams.suggestedRulesFlyout.deleteActionDescription', + { + defaultMessage: 'Remove this suggested rule', + } +); + +const FLYOUT_TITLE = i18n.translate('xpack.streams.suggestedRulesFlyout.title', { + defaultMessage: 'Suggested rules', +}); + +const FLYOUT_DESCRIPTION = i18n.translate('xpack.streams.suggestedRulesFlyout.description', { + defaultMessage: + 'We generate rules based on the queries that are of critical importance for this stream. You can review and discard results.', +}); + +const SHOWING_RULES_LABEL = (count: number) => + i18n.translate('xpack.streams.suggestedRulesFlyout.showing', { + defaultMessage: 'Showing {count, plural, one {# Rule} other {# Rules}}', + values: { count }, + }); + +const TABLE_CAPTION = i18n.translate('xpack.streams.suggestedRulesFlyout.tableCaption', { + defaultMessage: 'Suggested rules', +}); + +const NO_ITEMS_MESSAGE = i18n.translate('xpack.streams.suggestedRulesFlyout.noItems', { + defaultMessage: 'No suggested rules found.', +}); + +const CANCEL_BUTTON_LABEL = i18n.translate('xpack.streams.suggestedRulesFlyout.cancelButton', { + defaultMessage: 'Cancel', +}); + +const CREATE_RULES_BUTTON_LABEL = i18n.translate( + 'xpack.streams.suggestedRulesFlyout.createRulesButton', + { + defaultMessage: 'Create rules', + } +); + +const DETAILS_CLOSE_ARIA_LABEL = i18n.translate( + 'xpack.streams.suggestedRulesFlyout.detailsCloseAriaLabel', + { + defaultMessage: 'Close details', + } +); From 37852d3d0d38b123e140e89eda9bd3deb4212273 Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Wed, 1 Apr 2026 13:54:51 +0200 Subject: [PATCH 7/8] Toggles icons for KIs in the table --- .../index.tsx | 28 ++++++++----------- .../knowledge_indicators_table.tsx | 22 ++++----------- .../promotion_callout/promotion_callout.tsx | 2 +- .../rules_table/rules_table.tsx | 16 ++++------- .../utils/get_knowledge_indicator_item_id.ts | 13 +++++++++ 5 files changed, 36 insertions(+), 45 deletions(-) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/utils/get_knowledge_indicator_item_id.ts diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx index 2a973d3ddae1d..7d7267dbace49 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx @@ -41,6 +41,7 @@ import { RulesTable } from './rules_table'; import { LoadingPanel } from '../../loading_panel'; import { PromotionCallout } from './promotion_callout/promotion_callout'; import { SuggestedRulesFlyout } from './suggested_rules_flyout/suggested_rules_flyout'; +import { getKnowledgeIndicatorItemId } from './utils/get_knowledge_indicator_item_id'; const SEARCH_DEBOUNCE_MS = 300; @@ -152,6 +153,9 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { [knowledgeIndicators] ); const [isSuggestedRulesFlyoutOpen, setIsSuggestedRulesFlyoutOpen] = useState(false); + const selectedKnowledgeIndicatorId = selectedKnowledgeIndicator + ? getKnowledgeIndicatorItemId(selectedKnowledgeIndicator) + : undefined; const toggleSelectedKnowledgeIndicator = useCallback((knowledgeIndicator: KnowledgeIndicator) => { setSelectedKnowledgeIndicator((currentKnowledgeIndicator) => { @@ -159,14 +163,8 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { return knowledgeIndicator; } - const currentId = - currentKnowledgeIndicator.kind === 'feature' - ? currentKnowledgeIndicator.feature.uuid - : currentKnowledgeIndicator.query.id; - const nextId = - knowledgeIndicator.kind === 'feature' - ? knowledgeIndicator.feature.uuid - : knowledgeIndicator.query.id; + const currentId = getKnowledgeIndicatorItemId(currentKnowledgeIndicator); + const nextId = getKnowledgeIndicatorItemId(knowledgeIndicator); return currentId === nextId ? null : knowledgeIndicator; }); @@ -200,12 +198,10 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { return ( <> - - setIsSuggestedRulesFlyoutOpen(true)} - /> - + setIsSuggestedRulesFlyoutOpen(true)} + /> @@ -274,7 +270,7 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { rules={ruleKnowledgeIndicators} occurrencesByQueryId={occurrencesByQueryId} searchTerm={debouncedTableSearchValue} - selectedKnowledgeIndicator={selectedKnowledgeIndicator} + selectedKnowledgeIndicatorId={selectedKnowledgeIndicatorId} onViewDetails={toggleSelectedKnowledgeIndicator} /> ) : ( @@ -285,7 +281,7 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { searchTerm={debouncedTableSearchValue} selectedTypes={selectedKnowledgeIndicatorTypes} statusFilter={knowledgeIndicatorStatusFilter} - selectedKnowledgeIndicator={selectedKnowledgeIndicator} + selectedKnowledgeIndicatorId={selectedKnowledgeIndicatorId} onViewDetails={toggleSelectedKnowledgeIndicator} /> )} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx index 123d52d5f8085..a6b180152d9bd 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx @@ -28,6 +28,7 @@ import { KnowledgeIndicatorActionsCell } from '../knowledge_indicator_actions_ce import { DeleteTableItemsModal } from '../delete_table_items_modal'; import { SparkPlot } from '../../../spark_plot'; import { TableTitle } from '../../stream_detail_systems/table_title'; +import { getKnowledgeIndicatorItemId } from '../utils/get_knowledge_indicator_item_id'; interface KnowledgeIndicatorsTableProps { definition: Streams.all.Definition; @@ -36,18 +37,10 @@ interface KnowledgeIndicatorsTableProps { searchTerm: string; selectedTypes: string[]; statusFilter: 'active' | 'excluded'; - selectedKnowledgeIndicator: KnowledgeIndicator | null; + selectedKnowledgeIndicatorId?: string; onViewDetails: (knowledgeIndicator: KnowledgeIndicator) => void; } -const getKnowledgeIndicatorItemId = (knowledgeIndicator: KnowledgeIndicator) => { - if (knowledgeIndicator.kind === 'feature') { - return `feature:${knowledgeIndicator.feature.uuid}`; - } - - return `query:${knowledgeIndicator.query.id}`; -}; - export function KnowledgeIndicatorsTable({ definition, knowledgeIndicators, @@ -55,7 +48,7 @@ export function KnowledgeIndicatorsTable({ searchTerm, selectedTypes, statusFilter, - selectedKnowledgeIndicator, + selectedKnowledgeIndicatorId, onViewDetails, }: KnowledgeIndicatorsTableProps) { const [selectedKnowledgeIndicators, setSelectedKnowledgeIndicators] = useState< @@ -146,12 +139,7 @@ export function KnowledgeIndicatorsTable({ : knowledgeIndicator.query.title ?? knowledgeIndicator.query.id; const isExpanded = - (knowledgeIndicator.kind === 'feature' && - selectedKnowledgeIndicator?.kind === 'feature' && - selectedKnowledgeIndicator.feature.uuid === knowledgeIndicator.feature.uuid) || - (knowledgeIndicator.kind === 'query' && - selectedKnowledgeIndicator?.kind === 'query' && - selectedKnowledgeIndicator.query.id === knowledgeIndicator.query.id); + selectedKnowledgeIndicatorId === getKnowledgeIndicatorItemId(knowledgeIndicator); return ( @@ -230,7 +218,7 @@ export function KnowledgeIndicatorsTable({ ), }, ], - [definition, occurrencesByQueryId, onViewDetails, selectedKnowledgeIndicator] + [definition, occurrencesByQueryId, onViewDetails, selectedKnowledgeIndicatorId] ); return ( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx index d82de2ba5646f..b644ec26f1402 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/promotion_callout/promotion_callout.tsx @@ -134,7 +134,7 @@ export function PromotionCallout({ streamName, onReviewClick }: PromotionCallout data-test-subj="streamsAppPromotionCalloutPromoteButton" > {i18n.translate('xpack.streams.significantEvents.promotionCallout.promoteButton', { - defaultMessage: 'Promote', + defaultMessage: 'Create rules', })} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx index 8c961375a21ac..47211ade4fe7d 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx @@ -34,7 +34,7 @@ interface RulesTableProps { rules: KnowledgeIndicator[]; occurrencesByQueryId: Record>; searchTerm: string; - selectedKnowledgeIndicator: KnowledgeIndicator | null; + selectedKnowledgeIndicatorId?: string; onViewDetails: (knowledgeIndicator: KnowledgeIndicator) => void; } @@ -43,7 +43,7 @@ export function RulesTable({ rules, occurrencesByQueryId, searchTerm, - selectedKnowledgeIndicator, + selectedKnowledgeIndicatorId, onViewDetails, }: RulesTableProps) { const [selectedRules, setSelectedRules] = useState([]); @@ -113,15 +113,9 @@ export function RulesTable({ + knowledgeIndicator.kind === 'feature' + ? knowledgeIndicator.feature.uuid + : knowledgeIndicator.query.id; From 9e437e8b0028c7b495da6c548a1350037d204925 Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Wed, 1 Apr 2026 15:16:18 +0200 Subject: [PATCH 8/8] fixup! Toggles icons for KIs in the table --- .../suggested_rules_flyout/suggested_rules_flyout.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx index 84d563a523e53..7dbe1a255f23a 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/suggested_rules_flyout/suggested_rules_flyout.tsx @@ -371,10 +371,3 @@ const CREATE_RULES_BUTTON_LABEL = i18n.translate( defaultMessage: 'Create rules', } ); - -const DETAILS_CLOSE_ARIA_LABEL = i18n.translate( - 'xpack.streams.suggestedRulesFlyout.detailsCloseAriaLabel', - { - defaultMessage: 'Close details', - } -);