From 6900def97330ffe900e4bddfb951da30906bb156 Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Thu, 16 Apr 2026 16:34:15 +0200 Subject: [PATCH 01/11] refactor: move shared onboarding components from streams_view/ to shared/ Mechanical move of reusable components (GenerateSplitButton, ContextMenuSplitButton, ConnectorSubPanel, context_menu_helpers, translations, types) into a shared/ directory so they can be consumed by the Knowledge Indicators tab. Original files deleted. Made-with: Cursor --- .../connector_sub_panel.tsx | 0 .../context_menu_helpers.tsx | 0 .../context_menu_split_button.tsx | 0 .../generate_split_button.tsx | 3 + .../components/shared/translations.ts | 86 +++++++++++++++++ .../{streams_view => shared}/types.ts | 0 .../components/streams_view/translations.ts | 92 +++---------------- .../hooks/sig_events/use_connector_config.ts | 2 +- 8 files changed, 104 insertions(+), 79 deletions(-) rename x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/{streams_view => shared}/connector_sub_panel.tsx (100%) rename x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/{streams_view => shared}/context_menu_helpers.tsx (100%) rename x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/{streams_view => shared}/context_menu_split_button.tsx (100%) rename x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/{streams_view => shared}/generate_split_button.tsx (98%) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts rename x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/{streams_view => shared}/types.ts (100%) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/connector_sub_panel.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/connector_sub_panel.tsx similarity index 100% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/connector_sub_panel.tsx rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/connector_sub_panel.tsx diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_helpers.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/context_menu_helpers.tsx similarity index 100% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_helpers.tsx rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/context_menu_helpers.tsx diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_split_button.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/context_menu_split_button.tsx similarity index 100% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/context_menu_split_button.tsx rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/context_menu_split_button.tsx diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/generate_split_button.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/generate_split_button.tsx similarity index 98% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/generate_split_button.tsx rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/generate_split_button.tsx index 751be327ae749..14ccf8b5ff505 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/generate_split_button.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/generate_split_button.tsx @@ -33,6 +33,7 @@ interface GenerateSplitButtonProps { onRunQueriesOnly: () => void; isRunDisabled: boolean; isConfigDisabled: boolean; + isLoading?: boolean; } export const GenerateSplitButton = ({ @@ -47,6 +48,7 @@ export const GenerateSplitButton = ({ onRunQueriesOnly, isRunDisabled, isConfigDisabled, + isLoading, }: GenerateSplitButtonProps) => { const featuresConnector = useMemo( () => allConnectors.find((c) => c.connectorId === config.connectors.features), @@ -154,6 +156,7 @@ export const GenerateSplitButton = ({ buildPanels={buildPanels} error={connectorError} errorTitle={CONNECTOR_LOAD_ERROR} + isLoading={isLoading} data-test-subj="significant_events_generate_split_button" /> ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts new file mode 100644 index 0000000000000..f10999fce6c0b --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const GENERATE_FEATURES_BUTTON_LABEL = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.generateFeaturesButtonLabel', + { + defaultMessage: 'Generate KI Features', + } +); + +export const GENERATE_QUERIES_BUTTON_LABEL = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.generateQueriesButtonLabel', + { + defaultMessage: 'Generate KI Queries', + } +); + +export const CONNECTOR_LOAD_ERROR = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.connectorLoadError', + { + defaultMessage: 'Failed to load connectors', + } +); + +export const GENERATE_CONFIG_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.generateConfigAriaLabel', + { + defaultMessage: 'Configure generation steps and models', + } +); + +export const GENERATE_BUTTON_LABEL = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.generateButtonLabel', + { + defaultMessage: 'Generate', + } +); + +export const MODEL_SELECTION_PANEL_TITLE = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.modelSelectionPanelTitle', + { + defaultMessage: 'Model selection', + } +); + +export const MODEL_SETTINGS_LABEL = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.modelSettingsLabel', + { + defaultMessage: 'Model settings', + } +); + +export const DEFAULT_MODEL_BADGE_LABEL = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.defaultModelBadgeLabel', + { + defaultMessage: 'Default', + } +); + +export const GENERATE_FEATURES_TOOLTIP = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.generateFeaturesTooltip', + { + defaultMessage: + 'Runs only feature identification on selected streams using the configured model.', + } +); + +export const GENERATE_QUERIES_TOOLTIP = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.generateQueriesTooltip', + { + defaultMessage: 'Runs only query generation on selected streams using the configured model.', + } +); + +export const ONBOARDING_SCHEDULING_FAILURE_TITLE = i18n.translate( + 'xpack.streams.significantEventsDiscovery.streamsView.schedulingErrorTitle', + { + defaultMessage: 'Could not schedule a task to onboard stream', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/types.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/types.ts similarity index 100% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/types.ts rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/types.ts diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/translations.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/translations.ts index 9b7e3601954eb..7baaea46cf6f2 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/translations.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/translations.ts @@ -7,6 +7,20 @@ import { i18n } from '@kbn/i18n'; +export { + GENERATE_FEATURES_BUTTON_LABEL, + GENERATE_QUERIES_BUTTON_LABEL, + CONNECTOR_LOAD_ERROR, + GENERATE_CONFIG_ARIA_LABEL, + GENERATE_BUTTON_LABEL, + MODEL_SELECTION_PANEL_TITLE, + MODEL_SETTINGS_LABEL, + DEFAULT_MODEL_BADGE_LABEL, + GENERATE_FEATURES_TOOLTIP, + GENERATE_QUERIES_TOOLTIP, + ONBOARDING_SCHEDULING_FAILURE_TITLE, +} from '../shared/translations'; + export const NAME_COLUMN_HEADER = i18n.translate('xpack.streams.streamsTreeTable.nameColumnName', { defaultMessage: 'Name', }); @@ -100,13 +114,6 @@ export const ONBOARDING_FAILURE_TITLE = i18n.translate( } ); -export const ONBOARDING_SCHEDULING_FAILURE_TITLE = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.schedulingErrorTitle', - { - defaultMessage: 'Could not schedule a task to onboard stream', - } -); - export const DISCOVER_INSIGHTS_BUTTON_LABEL = i18n.translate( 'xpack.streams.significantEventsDiscovery.streamsView.discoverInsightsButtonLabel', { @@ -146,77 +153,6 @@ export const NO_INSIGHTS_TOAST_TITLE = i18n.translate( } ); -export const GENERATE_FEATURES_BUTTON_LABEL = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.generateFeaturesButtonLabel', - { - defaultMessage: 'Generate KI Features', - } -); - -export const GENERATE_QUERIES_BUTTON_LABEL = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.generateQueriesButtonLabel', - { - defaultMessage: 'Generate KI Queries', - } -); - -export const CONNECTOR_LOAD_ERROR = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.connectorLoadError', - { - defaultMessage: 'Failed to load connectors', - } -); - -export const GENERATE_CONFIG_ARIA_LABEL = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.generateConfigAriaLabel', - { - defaultMessage: 'Configure generation steps and models', - } -); - -export const GENERATE_BUTTON_LABEL = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.generateButtonLabel', - { - defaultMessage: 'Generate', - } -); - -export const MODEL_SELECTION_PANEL_TITLE = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.modelSelectionPanelTitle', - { - defaultMessage: 'Model selection', - } -); - -export const MODEL_SETTINGS_LABEL = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.modelSettingsLabel', - { - defaultMessage: 'Model settings', - } -); - -export const DEFAULT_MODEL_BADGE_LABEL = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.defaultModelBadgeLabel', - { - defaultMessage: 'Default', - } -); - -export const GENERATE_FEATURES_TOOLTIP = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.generateFeaturesTooltip', - { - defaultMessage: - 'Runs only feature identification on selected streams using the configured model.', - } -); - -export const GENERATE_QUERIES_TOOLTIP = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.generateQueriesTooltip', - { - defaultMessage: 'Runs only query generation on selected streams using the configured model.', - } -); - export const DISCOVER_INSIGHTS_CONFIG_ARIA_LABEL = i18n.translate( 'xpack.streams.significantEventsDiscovery.streamsView.discoverInsightsConfigAriaLabel', { diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_connector_config.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_connector_config.ts index 967a8c07d7df7..f0bb3e8dbb156 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_connector_config.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_connector_config.ts @@ -13,7 +13,7 @@ import { } from '@kbn/streams-schema'; import type { InferenceConnector } from '@kbn/inference-common'; import { useEffect, useState } from 'react'; -import type { OnboardingConfig } from '../../components/sig_events/significant_events_discovery/components/streams_view/types'; +import type { OnboardingConfig } from '../../components/sig_events/significant_events_discovery/components/shared/types'; import { useAIFeatures } from '../use_ai_features'; import { useInferenceFeatureConnectors } from './use_inference_feature_connectors'; From 56f5fa9db753bdf69f5f95fca9cd90d1b397f47e Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Thu, 16 Apr 2026 17:01:01 +0200 Subject: [PATCH 02/11] refactor(streams_app): centralize KI generation state in provider Move bulk onboarding, connector config, and stream fetching into a shared KiGenerationProvider so both the Streams and Knowledge Indicators tabs consume one source of truth. Add an inline generation row with StreamPicker on the KI tab, a progress callout, and a hidden-computed- features hint. Fix broken imports in insights_split_button pointing at the old streams_view/ location. Made-with: Cursor --- .../knowledge_indicators_table/index.ts | 1 + .../ki_generation_context.tsx | 280 ++++++++++++++++++ .../knowledge_indicators_table.tsx | 143 ++++++++- .../translations.ts | 24 +- .../use_knowledge_indicators_table.ts | 8 + .../components/shared/stream_picker.tsx | 83 ++++++ .../streams_view/insights_split_button.tsx | 9 +- .../components/streams_view/streams_view.tsx | 188 ++++-------- .../components/streams_view/translations.ts | 14 +- .../hooks/use_bulk_onboarding.ts | 112 +++++++ .../significant_events_discovery/page.tsx | 23 +- 11 files changed, 709 insertions(+), 176 deletions(-) create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/stream_picker.tsx create mode 100644 x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts index adade775c0472..4fd71c8a8040c 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts @@ -6,3 +6,4 @@ */ export { KnowledgeIndicatorsTable } from './knowledge_indicators_table'; +export { KiGenerationProvider } from './ki_generation_context'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx new file mode 100644 index 0000000000000..06899e0608c26 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx @@ -0,0 +1,280 @@ +/* + * 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 { InferenceConnector } from '@kbn/inference-common'; +import type { ListStreamDetail } from '@kbn/streams-plugin/server/routes/internal/streams/crud/route'; +import type { OnboardingResult, TaskResult } from '@kbn/streams-schema'; +import { TaskStatus } from '@kbn/streams-schema'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useConnectorConfig } from '../../../../../hooks/sig_events/use_connector_config'; +import { useIndexPatternsConfig } from '../../../../../hooks/use_index_patterns_config'; +import type { ScheduleOnboardingOptions } from '../../../../../hooks/use_onboarding_api'; +import { useBulkOnboarding } from '../../hooks/use_bulk_onboarding'; +import { useFetchStreams } from '../../hooks/use_fetch_streams'; +import type { OnboardingConfig } from '../shared/types'; + +const IN_PROGRESS_STATUSES: readonly TaskStatus[] = [ + TaskStatus.InProgress, + TaskStatus.BeingCanceled, +]; + +type StreamStatusCallback = (streamName: string, taskResult: TaskResult) => void; + +interface ConnectorState { + resolvedConnectorId: string | undefined; + loading: boolean; +} + +interface KiGenerationContextValue { + filteredStreams: ListStreamDetail[] | undefined; + isStreamsLoading: boolean; + generatingStreamNames: string[]; + isGenerating: boolean; + isScheduling: boolean; + streamStatusMap: Record>; + generationCompletedAt: number | undefined; + onboardingConfig: OnboardingConfig; + setOnboardingConfig: (config: OnboardingConfig) => void; + allConnectors: InferenceConnector[]; + connectorError: Error | undefined; + featuresConnectors: ConnectorState; + queriesConnectors: ConnectorState; + isConnectorCatalogUnavailable: boolean; + bulkOnboardAll: (streamNames: string[]) => Promise; + bulkOnboardFeaturesOnly: (streamNames: string[]) => Promise; + bulkOnboardQueriesOnly: (streamNames: string[]) => Promise; + bulkScheduleOnboardingTask: ( + streamNames: string[], + options?: ScheduleOnboardingOptions + ) => Promise; + cancelOnboardingTask: (streamName: string) => Promise; + isStreamActionable: (streamName: string) => boolean; + registerStatusCallback: (cb: StreamStatusCallback) => () => void; +} + +const KiGenerationReactContext = createContext(null); + +export function KiGenerationProvider({ children }: { children: React.ReactNode }) { + const [generatingStreams, setGeneratingStreams] = useState>(new Set()); + const [streamStatusMap, setStreamStatusMap] = useState< + Record> + >({}); + const [generationCompletedAt, setGenerationCompletedAt] = useState(undefined); + const statusCallbacksRef = useRef>(new Set()); + const prevGeneratingSizeRef = useRef(0); + // Ref-based so callbacks read the latest value without stale closures, and + // the provider can gate forwarding without needing a re-render. + const initialStatusFetchDoneRef = useRef(false); + + const { filterStreamsByIndexPatterns } = useIndexPatternsConfig(); + const { + onboardingConfig, + setOnboardingConfig, + allConnectors, + connectorError, + featuresConnectors, + queriesConnectors, + isConnectorCatalogUnavailable, + } = useConnectorConfig(); + + const streamsListFetch = useFetchStreams({ + select: (result) => ({ + ...result, + streams: filterStreamsByIndexPatterns(result.streams), + }), + }); + const filteredStreams = streamsListFetch.data?.streams; + const isStreamsLoading = streamsListFetch.isLoading; + + const registerStatusCallback = useCallback((cb: StreamStatusCallback) => { + statusCallbacksRef.current.add(cb); + return () => { + statusCallbacksRef.current.delete(cb); + }; + }, []); + + useEffect(() => { + if (prevGeneratingSizeRef.current > 0 && generatingStreams.size === 0) { + setGenerationCompletedAt(Date.now()); + } + prevGeneratingSizeRef.current = generatingStreams.size; + }, [generatingStreams]); + + const markAsGenerating = useCallback((streamNames: string[]) => { + if (streamNames.length === 0) return; + setGeneratingStreams((current) => { + const next = new Set(current); + streamNames.forEach((s) => next.add(s)); + return next; + }); + }, []); + + // Bidirectional: adds streams discovered as InProgress (e.g. on initial status + // fetch after page refresh) and removes streams that reach a terminal state. + // Callback forwarding is gated on the initial-fetch flag so initial-load + // updates don't trigger consumer side effects (like error toasts). + const onStreamStatusUpdate = useCallback( + (streamName: string, taskResult: TaskResult) => { + setStreamStatusMap((current) => ({ ...current, [streamName]: taskResult })); + + const isInProgress = IN_PROGRESS_STATUSES.includes(taskResult.status); + + setGeneratingStreams((current) => { + const has = current.has(streamName); + if (isInProgress === has) return current; + const next = new Set(current); + if (isInProgress) { + next.add(streamName); + } else { + next.delete(streamName); + } + return next; + }); + + if (initialStatusFetchDoneRef.current) { + statusCallbacksRef.current.forEach((cb) => cb(streamName, taskResult)); + } + }, + [] + ); + + const bulkOnboarding = useBulkOnboarding({ onboardingConfig, onStreamStatusUpdate }); + const { + onboardingStatusUpdateQueue, + processStatusUpdateQueue, + bulkOnboardAll: rawBulkOnboardAll, + bulkOnboardFeaturesOnly: rawBulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly: rawBulkOnboardQueriesOnly, + bulkScheduleOnboardingTask: rawBulkScheduleOnboardingTask, + } = bulkOnboarding; + + useEffect(() => { + if (!filteredStreams) return; + + filteredStreams.forEach((item) => { + onboardingStatusUpdateQueue.add(item.stream.name); + }); + processStatusUpdateQueue().finally(() => { + initialStatusFetchDoneRef.current = true; + }); + }, [filteredStreams, onboardingStatusUpdateQueue, processStatusUpdateQueue]); + + const isGenerating = generatingStreams.size > 0; + const generatingStreamNames = useMemo(() => Array.from(generatingStreams), [generatingStreams]); + + // Wrap bulk onboard methods with optimistic pre-fill so the UI immediately + // reflects streams as generating before the scheduling round-trip resolves. + const bulkOnboardAll = useCallback( + async (streamNames: string[]) => { + markAsGenerating(streamNames); + await rawBulkOnboardAll(streamNames); + }, + [markAsGenerating, rawBulkOnboardAll] + ); + + const bulkOnboardFeaturesOnly = useCallback( + async (streamNames: string[]) => { + markAsGenerating(streamNames); + await rawBulkOnboardFeaturesOnly(streamNames); + }, + [markAsGenerating, rawBulkOnboardFeaturesOnly] + ); + + const bulkOnboardQueriesOnly = useCallback( + async (streamNames: string[]) => { + markAsGenerating(streamNames); + await rawBulkOnboardQueriesOnly(streamNames); + }, + [markAsGenerating, rawBulkOnboardQueriesOnly] + ); + + const bulkScheduleOnboardingTask = useCallback( + async (streamNames: string[], options?: ScheduleOnboardingOptions) => { + markAsGenerating(streamNames); + await rawBulkScheduleOnboardingTask(streamNames, options); + }, + [markAsGenerating, rawBulkScheduleOnboardingTask] + ); + + const isStreamActionable = useCallback( + (streamName: string) => { + const result = streamStatusMap[streamName]; + if (!result) return false; + return !IN_PROGRESS_STATUSES.includes(result.status); + }, + [streamStatusMap] + ); + + const value = useMemo( + () => ({ + isScheduling: bulkOnboarding.isScheduling, + cancelOnboardingTask: bulkOnboarding.cancelOnboardingTask, + filteredStreams, + isStreamsLoading, + generatingStreamNames, + isGenerating, + streamStatusMap, + generationCompletedAt, + onboardingConfig, + setOnboardingConfig, + allConnectors, + connectorError, + featuresConnectors, + queriesConnectors, + isConnectorCatalogUnavailable, + bulkOnboardAll, + bulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly, + bulkScheduleOnboardingTask, + isStreamActionable, + registerStatusCallback, + }), + [ + bulkOnboarding.isScheduling, + bulkOnboarding.cancelOnboardingTask, + filteredStreams, + isStreamsLoading, + generatingStreamNames, + isGenerating, + streamStatusMap, + generationCompletedAt, + onboardingConfig, + setOnboardingConfig, + allConnectors, + connectorError, + featuresConnectors, + queriesConnectors, + isConnectorCatalogUnavailable, + bulkOnboardAll, + bulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly, + bulkScheduleOnboardingTask, + isStreamActionable, + registerStatusCallback, + ] + ); + + return ( + {children} + ); +} + +export function useKiGeneration(): KiGenerationContextValue { + const context = useContext(KiGenerationReactContext); + if (!context) { + throw new Error('useKiGeneration must be used within KiGenerationProvider'); + } + return context; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx index ac0b78482970a..6a3ffe162eb51 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx @@ -6,23 +6,28 @@ */ import { - EuiButtonEmpty, + EuiCallOut, EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, EuiHorizontalRule, EuiInMemoryTable, + EuiLoadingSpinner, EuiPanel, EuiSpacer, useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; import type { KnowledgeIndicator } from '@kbn/streams-ai'; -import React from 'react'; -import { useStreamsAppRouter } from '../../../../../hooks/use_streams_app_router'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { AssetImage } from '../../../../asset_image'; import { LoadingPanel } from '../../../../loading_panel'; import { KnowledgeIndicatorDetailsFlyout } from '../../../stream_detail_significant_events_view/knowledge_indicator_details_flyout'; import { DeleteTableItemsModal } from '../../../stream_detail_significant_events_view/delete_table_items_modal'; import { getKnowledgeIndicatorItemId } from '../../../stream_detail_significant_events_view/utils/get_knowledge_indicator_item_id'; +import { GenerateSplitButton } from '../shared/generate_split_button'; +import { StreamPicker } from '../shared/stream_picker'; +import { useKiGeneration } from './ki_generation_context'; import { useKnowledgeIndicatorsTable } from './use_knowledge_indicators_table'; import { useKnowledgeIndicatorsColumns } from './use_knowledge_indicators_columns'; import { KnowledgeIndicatorsToolbar } from './knowledge_indicators_toolbar'; @@ -31,19 +36,68 @@ import { NO_ITEMS_MESSAGE, EMPTY_STATE_TITLE, EMPTY_STATE_DESCRIPTION, - EMPTY_STATE_GO_TO_STREAMS, DELETE_MODAL_TITLE, + HIDDEN_COMPUTED_FEATURES_HINT, + GENERATION_IN_PROGRESS_TITLE, + getGenerationInProgressDescription, } from './translations'; export function KnowledgeIndicatorsTable() { - const router = useStreamsAppRouter(); const { euiTheme } = useEuiTheme(); + const [generationStreamNames, setGenerationStreamNames] = useState([]); + + const { + generatingStreamNames, + isGenerating, + isScheduling, + generationCompletedAt, + onboardingConfig, + setOnboardingConfig, + allConnectors, + connectorError, + featuresConnectors, + queriesConnectors, + isConnectorCatalogUnavailable, + bulkOnboardAll, + bulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly, + } = useKiGeneration(); + + const runAndClearPicker = useCallback( + (action: (names: string[]) => Promise) => { + void action(generationStreamNames); + setGenerationStreamNames([]); + }, + [generationStreamNames] + ); + + const onRunGeneration = useCallback( + () => runAndClearPicker(bulkOnboardAll), + [runAndClearPicker, bulkOnboardAll] + ); + const onRunFeaturesOnly = useCallback( + () => runAndClearPicker(bulkOnboardFeaturesOnly), + [runAndClearPicker, bulkOnboardFeaturesOnly] + ); + const onRunQueriesOnly = useCallback( + () => runAndClearPicker(bulkOnboardQueriesOnly), + [runAndClearPicker, bulkOnboardQueriesOnly] + ); + + const isRunDisabled = + generationStreamNames.length === 0 || + isConnectorCatalogUnavailable || + featuresConnectors.loading || + queriesConnectors.loading || + isScheduling; + const { knowledgeIndicators, occurrencesByQueryId, isLoading, isEmpty, + refetch, filteredKnowledgeIndicators, selectedKnowledgeIndicator, selectedKnowledgeIndicatorId, @@ -57,6 +111,7 @@ export function KnowledgeIndicatorsTable() { isOperationInProgress, selectionContainsNonExcludable, isSelectionActionsDisabled, + hasOnlyHiddenComputedFeatures, tableSearchValue, debouncedSearchTerm, statusFilter, @@ -76,6 +131,17 @@ export function KnowledgeIndicatorsTable() { deleteKnowledgeIndicatorsInBulk, } = useKnowledgeIndicatorsTable(); + const lastHandledGenerationCompletedAt = useRef(undefined); + useEffect(() => { + if ( + generationCompletedAt !== undefined && + generationCompletedAt !== lastHandledGenerationCompletedAt.current + ) { + lastHandledGenerationCompletedAt.current = generationCompletedAt; + refetch(); + } + }, [generationCompletedAt, refetch]); + const columns = useKnowledgeIndicatorsColumns({ occurrencesByQueryId, selectedKnowledgeIndicatorId, @@ -83,11 +149,56 @@ export function KnowledgeIndicatorsTable() { setKnowledgeIndicatorsToDelete, }); + const generationRow = ( + + + + + + + + + ); + + const generationProgressCallout = isGenerating ? ( + <> + + +

{getGenerationInProgressDescription(generatingStreamNames)}

+
+ + ) : null; + if (isLoading) { return ; } - if (isEmpty) { + if (isEmpty && !isGenerating) { return ( } title={

{EMPTY_STATE_TITLE}

} body={

{EMPTY_STATE_DESCRIPTION}

} - actions={ - - {EMPTY_STATE_GO_TO_STREAMS} - - } + actions={generationRow} /> ); } return ( + {generationRow} + {generationProgressCallout} + + {hasOnlyHiddenComputedFeatures && ( + <> + + + + )} + i18n.translate('xpack.streams.knowledgeIndicators.generationInProgressDescription', { + defaultMessage: 'Generation is running for: {streams}. This may take a few minutes.', + values: { streams: streamNames.join(', ') }, + }); + +export const HIDDEN_COMPUTED_FEATURES_HINT = i18n.translate( + 'xpack.streams.knowledgeIndicators.hiddenComputedFeaturesHint', + { + defaultMessage: + 'There are computed features hidden. Enable "Show computed features" to see them.', + } ); export const CANNOT_EXCLUDE_SELECTION_TOOLTIP = i18n.translate( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_table.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_table.ts index d0dfa3b0bbf86..ed9c04851a1ad 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_table.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_table.ts @@ -297,6 +297,12 @@ export function useKnowledgeIndicatorsTable() { const isOperationInProgress = isDeleting || isBulkOperationInProgress || isRowActionInProgress; + const hasOnlyHiddenComputedFeatures = useMemo(() => { + if (!hideComputedTypes || knowledgeIndicators.length === 0) return false; + if (filteredKnowledgeIndicators.length > 0) return false; + return knowledgeIndicators.some((ki) => ki.kind === 'feature' && isComputedFeature(ki.feature)); + }, [hideComputedTypes, knowledgeIndicators, filteredKnowledgeIndicators]); + const { selectionContainsNonExcludable, isSelectionActionsDisabled } = useMemo(() => { const containsQueries = selectedKnowledgeIndicators.some((ki) => ki.kind === 'query'); const containsComputed = selectedKnowledgeIndicators.some( @@ -313,6 +319,7 @@ export function useKnowledgeIndicatorsTable() { occurrencesByQueryId, isLoading, isEmpty, + refetch, filteredKnowledgeIndicators, selectedKnowledgeIndicator, selectedKnowledgeIndicatorId, @@ -326,6 +333,7 @@ export function useKnowledgeIndicatorsTable() { isOperationInProgress, selectionContainsNonExcludable, isSelectionActionsDisabled, + hasOnlyHiddenComputedFeatures, tableSearchValue, debouncedSearchTerm, statusFilter, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/stream_picker.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/stream_picker.tsx new file mode 100644 index 0000000000000..3cb38d021af54 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/stream_picker.tsx @@ -0,0 +1,83 @@ +/* + * 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 { EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiComboBox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo } from 'react'; +import { useKiGeneration } from '../knowledge_indicators_table/ki_generation_context'; + +interface StreamPickerProps { + selectedStreamNames: string[]; + onSelectedStreamNamesChange: (streamNames: string[]) => void; + excludedStreamNames?: string[]; + isDisabled?: boolean; + fullWidth?: boolean; +} + +export const StreamPicker = ({ + selectedStreamNames, + onSelectedStreamNamesChange, + excludedStreamNames, + isDisabled, + fullWidth, +}: StreamPickerProps) => { + const { filteredStreams, isStreamsLoading } = useKiGeneration(); + + const excludedSet = useMemo(() => new Set(excludedStreamNames ?? []), [excludedStreamNames]); + + const options = useMemo>>( + () => + (filteredStreams ?? []) + .filter((s) => !excludedSet.has(s.stream.name)) + .map((s) => ({ + label: s.stream.name, + key: s.stream.name, + })), + [filteredStreams, excludedSet] + ); + + const selectedOptions = useMemo>>( + () => selectedStreamNames.map((name) => ({ label: name, key: name })), + [selectedStreamNames] + ); + + const handleChange = useCallback( + (nextOptions: Array>) => { + onSelectedStreamNamesChange(nextOptions.map((o) => o.label)); + }, + [onSelectedStreamNamesChange] + ); + + return ( + + ); +}; + +const STREAM_PICKER_ARIA_LABEL = i18n.translate( + 'xpack.streams.knowledgeIndicators.streamPickerAriaLabel', + { + defaultMessage: 'Select streams to generate knowledge indicators for', + } +); + +const STREAM_PICKER_PLACEHOLDER = i18n.translate( + 'xpack.streams.knowledgeIndicators.streamPickerPlaceholder', + { + defaultMessage: 'Select streams...', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx index b571f4b269d53..0344e2fc4be67 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx @@ -12,9 +12,12 @@ import { DISCOVER_INSIGHTS_BUTTON_LABEL, DISCOVER_INSIGHTS_CONFIG_ARIA_LABEL, } from './translations'; -import { buildConnectorMenuItem, buildConnectorSelectionPanel } from './context_menu_helpers'; -import { ContextMenuSplitButton } from './context_menu_split_button'; -import type { MenuHelpers } from './context_menu_split_button'; +import { + buildConnectorMenuItem, + buildConnectorSelectionPanel, +} from '../shared/context_menu_helpers'; +import { ContextMenuSplitButton } from '../shared/context_menu_split_button'; +import type { MenuHelpers } from '../shared/context_menu_split_button'; interface InsightsSplitButtonProps { allConnectors: InferenceConnector[]; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx index 1f6dcc9bce77e..7706beaab7628 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx @@ -10,24 +10,19 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSearchBar, EuiText } from '@el import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import type { OnboardingResult, TaskResult } from '@kbn/streams-schema'; -import { OnboardingStep, TaskStatus } from '@kbn/streams-schema'; -import pMap from 'p-map'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { TaskStatus } from '@kbn/streams-schema'; +import React, { useCallback, useEffect, useState } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import type { TableRow } from './utils'; -import { useIndexPatternsConfig } from '../../../../../hooks/use_index_patterns_config'; import { useKibana } from '../../../../../hooks/use_kibana'; import { useInsightsDiscoveryApi } from '../../../../../hooks/sig_events/use_insights_discovery_api'; import { useConnectorConfig } from '../../../../../hooks/sig_events/use_connector_config'; -import type { ScheduleOnboardingOptions } from '../../../../../hooks/use_onboarding_api'; -import { useOnboardingApi } from '../../../../../hooks/use_onboarding_api'; import { useStreamsAppRouter } from '../../../../../hooks/use_streams_app_router'; import { useTaskPolling } from '../../../../../hooks/use_task_polling'; import { getFormattedError } from '../../../../../util/errors'; import { StreamsAppSearchBar } from '../../../../streams_app_search_bar'; -import { useOnboardingStatusUpdateQueue } from '../../hooks/use_onboarding_status_update_queue'; -import { GenerateSplitButton } from './generate_split_button'; +import { useKiGeneration } from '../knowledge_indicators_table/ki_generation_context'; +import { GenerateSplitButton } from '../shared/generate_split_button'; import { InsightsSplitButton } from './insights_split_button'; import { getInsightsCompleteToastTitle, @@ -35,11 +30,9 @@ import { INSIGHTS_SCHEDULING_FAILURE_TITLE, NO_INSIGHTS_TOAST_TITLE, ONBOARDING_FAILURE_TITLE, - ONBOARDING_SCHEDULING_FAILURE_TITLE, STREAMS_TABLE_SEARCH_ARIA_LABEL, } from './translations'; import { StreamsTreeTable } from './tree_table'; -import { useFetchStreams } from '../../hooks/use_fetch_streams'; const datePickerStyle = css` .euiFormControlLayout, @@ -60,43 +53,52 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { notifications: { toasts }, }, } = useKibana(); - const isInitialStatusUpdateDone = useRef(false); const [searchQuery, setSearchQuery] = useState(); const [isWaitingForInsightsTask, setIsWaitingForInsightsTask] = useState(false); - const { filterStreamsByIndexPatterns } = useIndexPatternsConfig(); const { - featuresConnectors, - queriesConnectors, discoveryConnectors, - allConnectors, - connectorError, - isConnectorCatalogUnavailable, discoveryConnectorOverride, setDiscoveryConnectorOverride, displayDiscoveryConnectorId, + } = useConnectorConfig(); + + const { + filteredStreams, + isStreamsLoading, onboardingConfig, setOnboardingConfig, - } = useConnectorConfig(); + allConnectors, + connectorError, + featuresConnectors, + queriesConnectors, + isConnectorCatalogUnavailable, + streamStatusMap, + cancelOnboardingTask, + bulkScheduleOnboardingTask, + bulkOnboardAll, + bulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly, + isStreamActionable, + registerStatusCallback, + } = useKiGeneration(); - const streamsListFetch = useFetchStreams({ - select: (result) => { - return { - ...result, - /** - * Significant events discovery works with streams that match the configured index patterns. - */ - streams: filterStreamsByIndexPatterns(result.streams), - }; - }, - }); + useEffect(() => { + return registerStatusCallback((streamName, taskResult) => { + if (taskResult.status === TaskStatus.Failed) { + toasts.addError(getFormattedError(new Error(taskResult.error)), { + title: ONBOARDING_FAILURE_TITLE, + }); + } + + if (taskResult.status === TaskStatus.Completed) { + refreshUnbackedQueriesCount(); + } + }); + }, [registerStatusCallback, toasts, refreshUnbackedQueriesCount]); const [selectedStreams, setSelectedStreams] = useState([]); - const [streamOnboardingResultMap, setStreamOnboardingResultMap] = useState< - Record> - >({}); const router = useStreamsAppRouter(); - const { scheduleOnboardingTask, cancelOnboardingTask } = useOnboardingApi(); const { scheduleInsightsDiscoveryTask, getInsightsDiscoveryTaskStatus } = useInsightsDiscoveryApi(); const [{ value: insightsTask }, getInsightsTaskStatus] = useAsyncFn( @@ -129,7 +131,6 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { getInsightsTaskStatus, ]); - // When we started the insights task from this view and it completes, show toast useEffect(() => { if (!isWaitingForInsightsTask || !insightsTask) return; if (insightsTask.status !== TaskStatus.Completed && insightsTask.status !== TaskStatus.Failed) { @@ -176,63 +177,10 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { } }, [isWaitingForInsightsTask, insightsTask, toasts, router, core]); - const onStreamStatusUpdate = useCallback( - (streamName: string, taskResult: TaskResult) => { - setStreamOnboardingResultMap((currentMap) => ({ - ...currentMap, - [streamName]: taskResult, - })); - - /** - * Preventing showing error toasts and doing extra work - * for the initial status update when the page loads for - * the first time - */ - if (!isInitialStatusUpdateDone.current) { - return; - } - - if (taskResult.status === TaskStatus.Failed) { - toasts.addError(getFormattedError(new Error(taskResult.error)), { - title: ONBOARDING_FAILURE_TITLE, - }); - } - - if (taskResult.status === TaskStatus.Completed) { - refreshUnbackedQueriesCount(); - } - }, - [refreshUnbackedQueriesCount, toasts] - ); - const { onboardingStatusUpdateQueue, processStatusUpdateQueue } = - useOnboardingStatusUpdateQueue(onStreamStatusUpdate); - const handleQueryChange: EuiSearchBarProps['onChange'] = ({ query }) => { if (query) setSearchQuery(query); }; - useEffect(() => { - if (streamsListFetch.data === undefined) { - return; - } - - streamsListFetch.data.streams.forEach((item) => { - onboardingStatusUpdateQueue.add(item.stream.name); - }); - processStatusUpdateQueue().finally(() => { - isInitialStatusUpdateDone.current = true; - }); - }, [onboardingStatusUpdateQueue, processStatusUpdateQueue, streamsListFetch.data]); - - const isStreamActionable = useCallback( - (streamName: string) => { - const result = streamOnboardingResultMap[streamName]; - if (!result) return false; - return ![TaskStatus.InProgress, TaskStatus.BeingCanceled].includes(result.status); - }, - [streamOnboardingResultMap] - ); - const getActionableStreamNames = useCallback( () => selectedStreams @@ -241,55 +189,23 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { [selectedStreams, isStreamActionable] ); - const bulkScheduleOnboardingTask = useCallback( - async (streamList: string[], options?: ScheduleOnboardingOptions) => { - try { - await pMap( - streamList, - async (streamName) => { - await scheduleOnboardingTask(streamName, options); - }, - { concurrency: 10 } - ); - } catch (error) { - toasts.addError(getFormattedError(error), { title: ONBOARDING_SCHEDULING_FAILURE_TITLE }); - } - - streamList.forEach((streamName) => { - onboardingStatusUpdateQueue.add(streamName); - }); - processStatusUpdateQueue(); - }, - [scheduleOnboardingTask, toasts, onboardingStatusUpdateQueue, processStatusUpdateQueue] - ); - const onBulkOnboardStreamsClick = useCallback(async () => { const streamList = getActionableStreamNames(); setSelectedStreams([]); - await bulkScheduleOnboardingTask(streamList, onboardingConfig); - }, [getActionableStreamNames, bulkScheduleOnboardingTask, onboardingConfig]); + await bulkOnboardAll(streamList); + }, [getActionableStreamNames, bulkOnboardAll]); - const onBulkOnboardStep = useCallback( - async (step: OnboardingStep) => { - const streamList = getActionableStreamNames(); - setSelectedStreams([]); - await bulkScheduleOnboardingTask(streamList, { - steps: [step], - connectors: onboardingConfig.connectors, - }); - }, - [getActionableStreamNames, bulkScheduleOnboardingTask, onboardingConfig.connectors] - ); - - const onBulkOnboardFeaturesOnly = useCallback( - () => onBulkOnboardStep(OnboardingStep.FeaturesIdentification), - [onBulkOnboardStep] - ); + const onBulkOnboardFeaturesOnly = useCallback(async () => { + const streamList = getActionableStreamNames(); + setSelectedStreams([]); + await bulkOnboardFeaturesOnly(streamList); + }, [getActionableStreamNames, bulkOnboardFeaturesOnly]); - const onBulkOnboardQueriesOnly = useCallback( - () => onBulkOnboardStep(OnboardingStep.QueriesGeneration), - [onBulkOnboardStep] - ); + const onBulkOnboardQueriesOnly = useCallback(async () => { + const streamList = getActionableStreamNames(); + setSelectedStreams([]); + await bulkOnboardQueriesOnly(streamList); + }, [getActionableStreamNames, bulkOnboardQueriesOnly]); const onOnboardStreamActionClick = async (streamName: string) => { await bulkScheduleOnboardingTask([streamName]); @@ -357,7 +273,7 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { 'xpack.streams.significantEventsDiscovery.streamsTree.streamsCountLabel', { defaultMessage: '{count} streams', - values: { count: streamsListFetch.data?.streams.length ?? 0 }, + values: { count: filteredStreams?.length ?? 0 }, } )} @@ -365,9 +281,9 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { +) => void; + +interface UseBulkOnboardingOptions { + onboardingConfig: OnboardingConfig; + onStreamStatusUpdate: StreamStatusUpdateCallback; +} + +export function useBulkOnboarding({ + onboardingConfig, + onStreamStatusUpdate, +}: UseBulkOnboardingOptions) { + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); + + const { scheduleOnboardingTask, cancelOnboardingTask } = useOnboardingApi(); + const { onboardingStatusUpdateQueue, processStatusUpdateQueue } = + useOnboardingStatusUpdateQueue(onStreamStatusUpdate); + + const [isScheduling, setIsScheduling] = useState(false); + + const bulkScheduleOnboardingTask = useCallback( + async (streamNames: string[], options?: ScheduleOnboardingOptions) => { + setIsScheduling(true); + try { + await pMap( + streamNames, + async (streamName) => { + await scheduleOnboardingTask(streamName, options); + }, + { concurrency: 10 } + ); + } catch (error) { + toasts.addError(getFormattedError(error), { title: ONBOARDING_SCHEDULING_FAILURE_TITLE }); + } finally { + setIsScheduling(false); + } + + streamNames.forEach((streamName) => { + onboardingStatusUpdateQueue.add(streamName); + }); + processStatusUpdateQueue(); + }, + [scheduleOnboardingTask, toasts, onboardingStatusUpdateQueue, processStatusUpdateQueue] + ); + + const bulkOnboardAll = useCallback( + async (streamNames: string[]) => { + await bulkScheduleOnboardingTask(streamNames, onboardingConfig); + }, + [bulkScheduleOnboardingTask, onboardingConfig] + ); + + const bulkOnboardStep = useCallback( + async (streamNames: string[], step: OnboardingStep) => { + await bulkScheduleOnboardingTask(streamNames, { + steps: [step], + connectors: onboardingConfig.connectors, + }); + }, + [bulkScheduleOnboardingTask, onboardingConfig.connectors] + ); + + const bulkOnboardFeaturesOnly = useCallback( + async (streamNames: string[]) => { + await bulkOnboardStep(streamNames, OnboardingStep.FeaturesIdentification); + }, + [bulkOnboardStep] + ); + + const bulkOnboardQueriesOnly = useCallback( + async (streamNames: string[]) => { + await bulkOnboardStep(streamNames, OnboardingStep.QueriesGeneration); + }, + [bulkOnboardStep] + ); + + return { + isScheduling, + cancelOnboardingTask, + bulkScheduleOnboardingTask, + bulkOnboardAll, + bulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly, + onboardingStatusUpdateQueue, + processStatusUpdateQueue, + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx index b63cc49f60f19..a7313a1bc9269 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx @@ -25,7 +25,10 @@ import { useUnbackedQueriesCount } from '../../../hooks/sig_events/use_unbacked_ import { useDiscoverySettings } from './context'; import { RedirectTo } from '../../redirect_to'; import { StreamsAppPageTemplate } from '../../streams_app_page_template'; -import { KnowledgeIndicatorsTable } from './components/knowledge_indicators_table'; +import { + KnowledgeIndicatorsTable, + KiGenerationProvider, +} from './components/knowledge_indicators_table'; import { QueriesTable } from './components/queries_table/queries_table'; import { StreamsView } from './components/streams_view/streams_view'; import { InsightsTab } from './components/insights/tab'; @@ -182,14 +185,16 @@ export function SignificantEventsDiscoveryPage() { } tabs={tabs} /> - - {tab === 'streams' && } - {tab === 'knowledge_indicators' && } - {tab === 'queries' && } - {tab === 'significant_events' && } - {tab === 'memory' && isMemoryEnabled && } - {tab === 'settings' && } - + + + {tab === 'streams' && } + {tab === 'knowledge_indicators' && } + {tab === 'queries' && } + {tab === 'significant_events' && } + {tab === 'memory' && isMemoryEnabled && } + {tab === 'settings' && } + + ); } From 9bbd79fd204df7a37cc451e5b47fe371a2a9a9b8 Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Fri, 17 Apr 2026 08:50:05 +0200 Subject: [PATCH 03/11] fix(cr): code review --- .../knowledge_indicators_table/index.ts | 2 +- .../ki_generation_context.tsx | 29 ++++++++++++++ .../knowledge_indicators_table.tsx | 14 ++++--- .../components/streams_view/streams_view.tsx | 39 +++++-------------- .../hooks/use_bulk_onboarding.ts | 20 +++++++--- .../hooks/use_fetch_streams.ts | 4 +- .../significant_events_discovery/page.tsx | 39 ++++++++++++++++++- 7 files changed, 99 insertions(+), 48 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts index 4fd71c8a8040c..25a6635263a5e 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts @@ -6,4 +6,4 @@ */ export { KnowledgeIndicatorsTable } from './knowledge_indicators_table'; -export { KiGenerationProvider } from './ki_generation_context'; +export { KiGenerationProvider, useKiGeneration } from './ki_generation_context'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx index 06899e0608c26..2eec49a3404f3 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx @@ -40,6 +40,7 @@ interface ConnectorState { interface KiGenerationContextValue { filteredStreams: ListStreamDetail[] | undefined; isStreamsLoading: boolean; + isInitialGenerationStatusLoading: boolean; generatingStreamNames: string[]; isGenerating: boolean; isScheduling: boolean; @@ -51,7 +52,11 @@ interface KiGenerationContextValue { connectorError: Error | undefined; featuresConnectors: ConnectorState; queriesConnectors: ConnectorState; + discoveryConnectors: ConnectorState; isConnectorCatalogUnavailable: boolean; + discoveryConnectorOverride: string | undefined; + setDiscoveryConnectorOverride: (id: string | undefined) => void; + displayDiscoveryConnectorId: string | undefined; bulkOnboardAll: (streamNames: string[]) => Promise; bulkOnboardFeaturesOnly: (streamNames: string[]) => Promise; bulkOnboardQueriesOnly: (streamNames: string[]) => Promise; @@ -86,7 +91,11 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } connectorError, featuresConnectors, queriesConnectors, + discoveryConnectors, isConnectorCatalogUnavailable, + discoveryConnectorOverride, + setDiscoveryConnectorOverride, + displayDiscoveryConnectorId, } = useConnectorConfig(); const streamsListFetch = useFetchStreams({ @@ -174,6 +183,16 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } const isGenerating = generatingStreams.size > 0; const generatingStreamNames = useMemo(() => Array.from(generatingStreams), [generatingStreams]); + // True until we've received at least one status result for every filtered + // stream, so consumers can defer rendering empty/generating UI until the + // generating set is known. Once false, stays false — transient refetches of + // the streams list must not flash the loading panel again. + const isInitialGenerationStatusLoading = useMemo(() => { + if (initialStatusFetchDoneRef.current) return false; + if (isStreamsLoading || !filteredStreams) return true; + return filteredStreams.some((item) => !(item.stream.name in streamStatusMap)); + }, [isStreamsLoading, filteredStreams, streamStatusMap]); + // Wrap bulk onboard methods with optimistic pre-fill so the UI immediately // reflects streams as generating before the scheduling round-trip resolves. const bulkOnboardAll = useCallback( @@ -223,6 +242,7 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } cancelOnboardingTask: bulkOnboarding.cancelOnboardingTask, filteredStreams, isStreamsLoading, + isInitialGenerationStatusLoading, generatingStreamNames, isGenerating, streamStatusMap, @@ -233,7 +253,11 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } connectorError, featuresConnectors, queriesConnectors, + discoveryConnectors, isConnectorCatalogUnavailable, + discoveryConnectorOverride, + setDiscoveryConnectorOverride, + displayDiscoveryConnectorId, bulkOnboardAll, bulkOnboardFeaturesOnly, bulkOnboardQueriesOnly, @@ -246,6 +270,7 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } bulkOnboarding.cancelOnboardingTask, filteredStreams, isStreamsLoading, + isInitialGenerationStatusLoading, generatingStreamNames, isGenerating, streamStatusMap, @@ -256,7 +281,11 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } connectorError, featuresConnectors, queriesConnectors, + discoveryConnectors, isConnectorCatalogUnavailable, + discoveryConnectorOverride, + setDiscoveryConnectorOverride, + displayDiscoveryConnectorId, bulkOnboardAll, bulkOnboardFeaturesOnly, bulkOnboardQueriesOnly, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx index 6a3ffe162eb51..bb56fed18bdbb 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx @@ -50,6 +50,7 @@ export function KnowledgeIndicatorsTable() { const { generatingStreamNames, isGenerating, + isInitialGenerationStatusLoading, isScheduling, generationCompletedAt, onboardingConfig, @@ -65,23 +66,24 @@ export function KnowledgeIndicatorsTable() { } = useKiGeneration(); const runAndClearPicker = useCallback( - (action: (names: string[]) => Promise) => { - void action(generationStreamNames); + async (action: (names: string[]) => Promise) => { + const names = generationStreamNames; setGenerationStreamNames([]); + await action(names); }, [generationStreamNames] ); const onRunGeneration = useCallback( - () => runAndClearPicker(bulkOnboardAll), + async () => runAndClearPicker(bulkOnboardAll), [runAndClearPicker, bulkOnboardAll] ); const onRunFeaturesOnly = useCallback( - () => runAndClearPicker(bulkOnboardFeaturesOnly), + async () => runAndClearPicker(bulkOnboardFeaturesOnly), [runAndClearPicker, bulkOnboardFeaturesOnly] ); const onRunQueriesOnly = useCallback( - () => runAndClearPicker(bulkOnboardQueriesOnly), + async () => runAndClearPicker(bulkOnboardQueriesOnly), [runAndClearPicker, bulkOnboardQueriesOnly] ); @@ -194,7 +196,7 @@ export function KnowledgeIndicatorsTable() { ) : null; - if (isLoading) { + if (knowledgeIndicators.length === 0 && (isLoading || isInitialGenerationStatusLoading)) { return ; } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx index 7706beaab7628..a091062eb9bef 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx @@ -16,7 +16,6 @@ import useAsyncFn from 'react-use/lib/useAsyncFn'; import type { TableRow } from './utils'; import { useKibana } from '../../../../../hooks/use_kibana'; import { useInsightsDiscoveryApi } from '../../../../../hooks/sig_events/use_insights_discovery_api'; -import { useConnectorConfig } from '../../../../../hooks/sig_events/use_connector_config'; import { useStreamsAppRouter } from '../../../../../hooks/use_streams_app_router'; import { useTaskPolling } from '../../../../../hooks/use_task_polling'; import { getFormattedError } from '../../../../../util/errors'; @@ -29,7 +28,6 @@ import { INSIGHTS_COMPLETE_TOAST_VIEW_BUTTON, INSIGHTS_SCHEDULING_FAILURE_TITLE, NO_INSIGHTS_TOAST_TITLE, - ONBOARDING_FAILURE_TITLE, STREAMS_TABLE_SEARCH_ARIA_LABEL, } from './translations'; import { StreamsTreeTable } from './tree_table'; @@ -42,11 +40,7 @@ const datePickerStyle = css` } `; -interface StreamsViewProps { - refreshUnbackedQueriesCount: () => void; -} - -export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { +export function StreamsView() { const { core, core: { @@ -56,23 +50,21 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { const [searchQuery, setSearchQuery] = useState(); const [isWaitingForInsightsTask, setIsWaitingForInsightsTask] = useState(false); - const { - discoveryConnectors, - discoveryConnectorOverride, - setDiscoveryConnectorOverride, - displayDiscoveryConnectorId, - } = useConnectorConfig(); - const { filteredStreams, isStreamsLoading, + isScheduling, onboardingConfig, setOnboardingConfig, allConnectors, connectorError, featuresConnectors, queriesConnectors, + discoveryConnectors, isConnectorCatalogUnavailable, + discoveryConnectorOverride, + setDiscoveryConnectorOverride, + displayDiscoveryConnectorId, streamStatusMap, cancelOnboardingTask, bulkScheduleOnboardingTask, @@ -80,23 +72,8 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { bulkOnboardFeaturesOnly, bulkOnboardQueriesOnly, isStreamActionable, - registerStatusCallback, } = useKiGeneration(); - useEffect(() => { - return registerStatusCallback((streamName, taskResult) => { - if (taskResult.status === TaskStatus.Failed) { - toasts.addError(getFormattedError(new Error(taskResult.error)), { - title: ONBOARDING_FAILURE_TITLE, - }); - } - - if (taskResult.status === TaskStatus.Completed) { - refreshUnbackedQueriesCount(); - } - }); - }, [registerStatusCallback, toasts, refreshUnbackedQueriesCount]); - const [selectedStreams, setSelectedStreams] = useState([]); const router = useStreamsAppRouter(); const { scheduleInsightsDiscoveryTask, getInsightsDiscoveryTaskStatus } = @@ -247,9 +224,11 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { selectedStreams.length === 0 || isConnectorCatalogUnavailable || featuresConnectors.loading || - queriesConnectors.loading + queriesConnectors.loading || + isScheduling } isConfigDisabled={selectedStreams.length === 0} + isLoading={isScheduling} /> diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts index 89d6bb9bf9c06..2dfabac8f5030 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts @@ -46,24 +46,32 @@ export function useBulkOnboarding({ const bulkScheduleOnboardingTask = useCallback( async (streamNames: string[], options?: ScheduleOnboardingOptions) => { setIsScheduling(true); + const succeeded: string[] = []; try { await pMap( streamNames, async (streamName) => { - await scheduleOnboardingTask(streamName, options); + try { + await scheduleOnboardingTask(streamName, options); + succeeded.push(streamName); + } catch (error) { + toasts.addError(getFormattedError(error), { + title: ONBOARDING_SCHEDULING_FAILURE_TITLE, + }); + } }, - { concurrency: 10 } + { concurrency: 10, stopOnError: false } ); - } catch (error) { - toasts.addError(getFormattedError(error), { title: ONBOARDING_SCHEDULING_FAILURE_TITLE }); } finally { setIsScheduling(false); } - streamNames.forEach((streamName) => { + succeeded.forEach((streamName) => { onboardingStatusUpdateQueue.add(streamName); }); - processStatusUpdateQueue(); + if (succeeded.length > 0) { + processStatusUpdateQueue(); + } }, [scheduleOnboardingTask, toasts, onboardingStatusUpdateQueue, processStatusUpdateQueue] ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_fetch_streams.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_fetch_streams.ts index 84fa1312a7663..d22fa33512070 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_fetch_streams.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_fetch_streams.ts @@ -10,7 +10,6 @@ import { useQuery } from '@kbn/react-query'; import type { ListStreamDetail } from '@kbn/streams-plugin/server/routes/internal/streams/crud/route'; import { useFetchErrorToast } from '../../../../hooks/use_fetch_error_toast'; import { useKibana } from '../../../../hooks/use_kibana'; -import { useTimefilter } from '../../../../hooks/use_timefilter'; interface StreamsFetchResult { streams: ListStreamDetail[]; @@ -29,7 +28,6 @@ export function useFetchStreams( }, }, } = useKibana(); - const { timeState } = useTimefilter(); const showFetchErrorToast = useFetchErrorToast(); const fetchStreams = async ({ signal }: QueryFunctionContext): Promise => { @@ -37,7 +35,7 @@ export function useFetchStreams( }; return useQuery({ - queryKey: ['streamList', timeState.start, timeState.end], + queryKey: ['streamList'], queryFn: fetchStreams, onError: showFetchErrorToast, select: options?.select, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx index a7313a1bc9269..713fb0fdef2bb 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx @@ -15,8 +15,11 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; +import { TaskStatus } from '@kbn/streams-schema'; +import React, { useEffect, useMemo } from 'react'; import { useIsMutating } from '@kbn/react-query'; +import { useKibana } from '../../../hooks/use_kibana'; +import { getFormattedError } from '../../../util/errors'; import { useStreamsAppBreadcrumbs } from '../../../hooks/use_streams_app_breadcrumbs'; import { useStreamsAppParams } from '../../../hooks/use_streams_app_params'; import { useStreamsAppRouter } from '../../../hooks/use_streams_app_router'; @@ -28,7 +31,9 @@ import { StreamsAppPageTemplate } from '../../streams_app_page_template'; import { KnowledgeIndicatorsTable, KiGenerationProvider, + useKiGeneration, } from './components/knowledge_indicators_table'; +import { ONBOARDING_FAILURE_TITLE } from './components/streams_view/translations'; import { QueriesTable } from './components/queries_table/queries_table'; import { StreamsView } from './components/streams_view/streams_view'; import { InsightsTab } from './components/insights/tab'; @@ -49,6 +54,35 @@ function isValidDiscoveryTab(value: string): value is DiscoveryTab { return discoveryTabs.includes(value as DiscoveryTab); } +function KiGenerationEffects({ + refreshUnbackedQueriesCount, +}: { + refreshUnbackedQueriesCount: () => void; +}) { + const { registerStatusCallback } = useKiGeneration(); + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); + + useEffect(() => { + return registerStatusCallback((_streamName, taskResult) => { + if (taskResult.status === TaskStatus.Failed) { + toasts.addError(getFormattedError(new Error(taskResult.error)), { + title: ONBOARDING_FAILURE_TITLE, + }); + } + + if (taskResult.status === TaskStatus.Completed) { + refreshUnbackedQueriesCount(); + } + }); + }, [registerStatusCallback, toasts, refreshUnbackedQueriesCount]); + + return null; +} + export function SignificantEventsDiscoveryPage() { const { path: { tab }, @@ -186,8 +220,9 @@ export function SignificantEventsDiscoveryPage() { tabs={tabs} /> + - {tab === 'streams' && } + {tab === 'streams' && } {tab === 'knowledge_indicators' && } {tab === 'queries' && } {tab === 'significant_events' && } From f7c0de7b4a73f41e1237483363487818b2f639cb Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Fri, 17 Apr 2026 10:06:54 +0200 Subject: [PATCH 04/11] fix(use inference feature connectors): call inference connectors api instead of useLoadConnectors --- .../use_inference_feature_connectors.ts | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts index e5437d8220b92..5b69fd327b232 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts @@ -5,29 +5,50 @@ * 2.0. */ -import { useLoadConnectors } from '@kbn/inference-connectors'; +import { useQuery } from '@kbn/react-query'; +import { + INFERENCE_CONNECTORS_INTERNAL_API_PATH, + type InferenceConnectorsApiResponseBody, +} from '@kbn/inference-common'; import { useKibana } from '../use_kibana'; +const NO_DEFAULT_CONNECTOR = 'NO_DEFAULT_CONNECTOR'; + export interface UseInferenceFeatureConnectorsResult { resolvedConnectorId: string | undefined; loading: boolean; error: Error | undefined; } +/** + * Resolves the connector to use for a given inference feature by calling + * the search_inference_endpoints API directly. This bypasses the + * default-prepending logic in useLoadConnectors, so we always get the + * feature-specific connector when one exists. + */ export function useInferenceFeatureConnectors( featureId: string ): UseInferenceFeatureConnectorsResult { const { core } = useKibana(); - const query = useLoadConnectors({ - http: core.http, - toasts: core.notifications.toasts, - featureId, - settings: core.settings, - }); + const query = useQuery( + ['streams-feature-connectors', featureId], + () => + core.http.get(INFERENCE_CONNECTORS_INTERNAL_API_PATH, { + query: { featureId }, + version: '1', + }), + { retry: false, keepPreviousData: true } + ); + + // Feature-specific connectors take priority over the full catalog. + const featureConnectors = query.data?.connectors ?? []; + const allConnectors = query.data?.allConnectors ?? []; + const raw = featureConnectors[0]?.connectorId ?? allConnectors[0]?.connectorId; + const resolvedConnectorId = raw === NO_DEFAULT_CONNECTOR ? undefined : raw; return { - resolvedConnectorId: query.data?.[0]?.id, + resolvedConnectorId, loading: query.isLoading, error: query.error ?? undefined, }; From 8b45bf0a1e4472af9d25fe9b4b41c684f3bac1d3 Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Fri, 17 Apr 2026 10:09:55 +0200 Subject: [PATCH 05/11] fix: unmark failed streams from generatingStreams after bulk scheduling bulkScheduleOnboardingTask now returns succeeded stream names. Wrappers in ki_generation_context compare requested vs succeeded and remove failed streams from generatingStreams so they don't appear stuck in generating state indefinitely. Made-with: Cursor --- .../ki_generation_context.tsx | 34 +++++++++++++------ .../hooks/use_bulk_onboarding.ts | 20 ++++++----- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx index 2eec49a3404f3..e9369a9ad580b 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx @@ -193,38 +193,52 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } return filteredStreams.some((item) => !(item.stream.name in streamStatusMap)); }, [isStreamsLoading, filteredStreams, streamStatusMap]); - // Wrap bulk onboard methods with optimistic pre-fill so the UI immediately - // reflects streams as generating before the scheduling round-trip resolves. + const unmarkFailedStreams = useCallback((requested: string[], succeeded: string[]) => { + if (succeeded.length === requested.length) return; + const succeededSet = new Set(succeeded); + const failed = requested.filter((s) => !succeededSet.has(s)); + if (failed.length === 0) return; + setGeneratingStreams((current) => { + const next = new Set(current); + failed.forEach((s) => next.delete(s)); + return next; + }); + }, []); + const bulkOnboardAll = useCallback( async (streamNames: string[]) => { markAsGenerating(streamNames); - await rawBulkOnboardAll(streamNames); + const succeeded = await rawBulkOnboardAll(streamNames); + unmarkFailedStreams(streamNames, succeeded); }, - [markAsGenerating, rawBulkOnboardAll] + [markAsGenerating, rawBulkOnboardAll, unmarkFailedStreams] ); const bulkOnboardFeaturesOnly = useCallback( async (streamNames: string[]) => { markAsGenerating(streamNames); - await rawBulkOnboardFeaturesOnly(streamNames); + const succeeded = await rawBulkOnboardFeaturesOnly(streamNames); + unmarkFailedStreams(streamNames, succeeded); }, - [markAsGenerating, rawBulkOnboardFeaturesOnly] + [markAsGenerating, rawBulkOnboardFeaturesOnly, unmarkFailedStreams] ); const bulkOnboardQueriesOnly = useCallback( async (streamNames: string[]) => { markAsGenerating(streamNames); - await rawBulkOnboardQueriesOnly(streamNames); + const succeeded = await rawBulkOnboardQueriesOnly(streamNames); + unmarkFailedStreams(streamNames, succeeded); }, - [markAsGenerating, rawBulkOnboardQueriesOnly] + [markAsGenerating, rawBulkOnboardQueriesOnly, unmarkFailedStreams] ); const bulkScheduleOnboardingTask = useCallback( async (streamNames: string[], options?: ScheduleOnboardingOptions) => { markAsGenerating(streamNames); - await rawBulkScheduleOnboardingTask(streamNames, options); + const succeeded = await rawBulkScheduleOnboardingTask(streamNames, options); + unmarkFailedStreams(streamNames, succeeded); }, - [markAsGenerating, rawBulkScheduleOnboardingTask] + [markAsGenerating, rawBulkScheduleOnboardingTask, unmarkFailedStreams] ); const isStreamActionable = useCallback( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts index 2dfabac8f5030..2c483aab25dc9 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts @@ -44,7 +44,7 @@ export function useBulkOnboarding({ const [isScheduling, setIsScheduling] = useState(false); const bulkScheduleOnboardingTask = useCallback( - async (streamNames: string[], options?: ScheduleOnboardingOptions) => { + async (streamNames: string[], options?: ScheduleOnboardingOptions): Promise => { setIsScheduling(true); const succeeded: string[] = []; try { @@ -72,20 +72,22 @@ export function useBulkOnboarding({ if (succeeded.length > 0) { processStatusUpdateQueue(); } + + return succeeded; }, [scheduleOnboardingTask, toasts, onboardingStatusUpdateQueue, processStatusUpdateQueue] ); const bulkOnboardAll = useCallback( - async (streamNames: string[]) => { - await bulkScheduleOnboardingTask(streamNames, onboardingConfig); + async (streamNames: string[]): Promise => { + return bulkScheduleOnboardingTask(streamNames, onboardingConfig); }, [bulkScheduleOnboardingTask, onboardingConfig] ); const bulkOnboardStep = useCallback( - async (streamNames: string[], step: OnboardingStep) => { - await bulkScheduleOnboardingTask(streamNames, { + async (streamNames: string[], step: OnboardingStep): Promise => { + return bulkScheduleOnboardingTask(streamNames, { steps: [step], connectors: onboardingConfig.connectors, }); @@ -94,15 +96,15 @@ export function useBulkOnboarding({ ); const bulkOnboardFeaturesOnly = useCallback( - async (streamNames: string[]) => { - await bulkOnboardStep(streamNames, OnboardingStep.FeaturesIdentification); + async (streamNames: string[]): Promise => { + return bulkOnboardStep(streamNames, OnboardingStep.FeaturesIdentification); }, [bulkOnboardStep] ); const bulkOnboardQueriesOnly = useCallback( - async (streamNames: string[]) => { - await bulkOnboardStep(streamNames, OnboardingStep.QueriesGeneration); + async (streamNames: string[]): Promise => { + return bulkOnboardStep(streamNames, OnboardingStep.QueriesGeneration); }, [bulkOnboardStep] ); From 3c45a5eea921ca35476ff2b8bb31d73f71f7e736 Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Fri, 17 Apr 2026 10:37:55 +0200 Subject: [PATCH 06/11] fix(cr): code review --- .../ki_generation_context.tsx | 46 ++++++++++++------- .../knowledge_indicators_table.tsx | 6 ++- .../components/shared/stream_picker.tsx | 12 +++-- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx index e9369a9ad580b..06047eebd11a6 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx @@ -57,13 +57,13 @@ interface KiGenerationContextValue { discoveryConnectorOverride: string | undefined; setDiscoveryConnectorOverride: (id: string | undefined) => void; displayDiscoveryConnectorId: string | undefined; - bulkOnboardAll: (streamNames: string[]) => Promise; - bulkOnboardFeaturesOnly: (streamNames: string[]) => Promise; - bulkOnboardQueriesOnly: (streamNames: string[]) => Promise; + bulkOnboardAll: (streamNames: string[]) => Promise; + bulkOnboardFeaturesOnly: (streamNames: string[]) => Promise; + bulkOnboardQueriesOnly: (streamNames: string[]) => Promise; bulkScheduleOnboardingTask: ( streamNames: string[], options?: ScheduleOnboardingOptions - ) => Promise; + ) => Promise; cancelOnboardingTask: (streamName: string) => Promise; isStreamActionable: (streamName: string) => boolean; registerStatusCallback: (cb: StreamStatusCallback) => () => void; @@ -79,9 +79,11 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } const [generationCompletedAt, setGenerationCompletedAt] = useState(undefined); const statusCallbacksRef = useRef>(new Set()); const prevGeneratingSizeRef = useRef(0); - // Ref-based so callbacks read the latest value without stale closures, and - // the provider can gate forwarding without needing a re-render. + // Ref gates callback forwarding without stale closures; state flag drives + // useMemo reactivity for isInitialGenerationStatusLoading. const initialStatusFetchDoneRef = useRef(false); + const [initialStatusFetchDone, setInitialStatusFetchDone] = useState(false); + const enqueuedStreamNamesRef = useRef>(new Set()); const { filterStreamsByIndexPatterns } = useIndexPatternsConfig(); const { @@ -172,12 +174,20 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } useEffect(() => { if (!filteredStreams) return; + let hasNew = false; filteredStreams.forEach((item) => { - onboardingStatusUpdateQueue.add(item.stream.name); - }); - processStatusUpdateQueue().finally(() => { - initialStatusFetchDoneRef.current = true; + if (!enqueuedStreamNamesRef.current.has(item.stream.name)) { + enqueuedStreamNamesRef.current.add(item.stream.name); + onboardingStatusUpdateQueue.add(item.stream.name); + hasNew = true; + } }); + if (hasNew) { + processStatusUpdateQueue().finally(() => { + initialStatusFetchDoneRef.current = true; + setInitialStatusFetchDone(true); + }); + } }, [filteredStreams, onboardingStatusUpdateQueue, processStatusUpdateQueue]); const isGenerating = generatingStreams.size > 0; @@ -188,10 +198,10 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } // generating set is known. Once false, stays false — transient refetches of // the streams list must not flash the loading panel again. const isInitialGenerationStatusLoading = useMemo(() => { - if (initialStatusFetchDoneRef.current) return false; + if (initialStatusFetchDone) return false; if (isStreamsLoading || !filteredStreams) return true; return filteredStreams.some((item) => !(item.stream.name in streamStatusMap)); - }, [isStreamsLoading, filteredStreams, streamStatusMap]); + }, [initialStatusFetchDone, isStreamsLoading, filteredStreams, streamStatusMap]); const unmarkFailedStreams = useCallback((requested: string[], succeeded: string[]) => { if (succeeded.length === requested.length) return; @@ -206,37 +216,41 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } }, []); const bulkOnboardAll = useCallback( - async (streamNames: string[]) => { + async (streamNames: string[]): Promise => { markAsGenerating(streamNames); const succeeded = await rawBulkOnboardAll(streamNames); unmarkFailedStreams(streamNames, succeeded); + return succeeded; }, [markAsGenerating, rawBulkOnboardAll, unmarkFailedStreams] ); const bulkOnboardFeaturesOnly = useCallback( - async (streamNames: string[]) => { + async (streamNames: string[]): Promise => { markAsGenerating(streamNames); const succeeded = await rawBulkOnboardFeaturesOnly(streamNames); unmarkFailedStreams(streamNames, succeeded); + return succeeded; }, [markAsGenerating, rawBulkOnboardFeaturesOnly, unmarkFailedStreams] ); const bulkOnboardQueriesOnly = useCallback( - async (streamNames: string[]) => { + async (streamNames: string[]): Promise => { markAsGenerating(streamNames); const succeeded = await rawBulkOnboardQueriesOnly(streamNames); unmarkFailedStreams(streamNames, succeeded); + return succeeded; }, [markAsGenerating, rawBulkOnboardQueriesOnly, unmarkFailedStreams] ); const bulkScheduleOnboardingTask = useCallback( - async (streamNames: string[], options?: ScheduleOnboardingOptions) => { + async (streamNames: string[], options?: ScheduleOnboardingOptions): Promise => { markAsGenerating(streamNames); const succeeded = await rawBulkScheduleOnboardingTask(streamNames, options); unmarkFailedStreams(streamNames, succeeded); + return succeeded; }, [markAsGenerating, rawBulkScheduleOnboardingTask, unmarkFailedStreams] ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx index bb56fed18bdbb..61d48be1b9b00 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx @@ -48,6 +48,8 @@ export function KnowledgeIndicatorsTable() { const [generationStreamNames, setGenerationStreamNames] = useState([]); const { + filteredStreams, + isStreamsLoading, generatingStreamNames, isGenerating, isInitialGenerationStatusLoading, @@ -66,7 +68,7 @@ export function KnowledgeIndicatorsTable() { } = useKiGeneration(); const runAndClearPicker = useCallback( - async (action: (names: string[]) => Promise) => { + async (action: (names: string[]) => Promise) => { const names = generationStreamNames; setGenerationStreamNames([]); await action(names); @@ -155,6 +157,8 @@ export function KnowledgeIndicatorsTable() { void; excludedStreamNames?: string[]; @@ -20,25 +22,25 @@ interface StreamPickerProps { } export const StreamPicker = ({ + streams, + isStreamsLoading, selectedStreamNames, onSelectedStreamNamesChange, excludedStreamNames, isDisabled, fullWidth, }: StreamPickerProps) => { - const { filteredStreams, isStreamsLoading } = useKiGeneration(); - const excludedSet = useMemo(() => new Set(excludedStreamNames ?? []), [excludedStreamNames]); const options = useMemo>>( () => - (filteredStreams ?? []) + (streams ?? []) .filter((s) => !excludedSet.has(s.stream.name)) .map((s) => ({ label: s.stream.name, key: s.stream.name, })), - [filteredStreams, excludedSet] + [streams, excludedSet] ); const selectedOptions = useMemo>>( From 9ddc95df856e3c7667cd751a42b2529fb5931177 Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Fri, 17 Apr 2026 11:51:54 +0200 Subject: [PATCH 07/11] refactor: simplify KiGenerationProvider and remove dead code Remove connector fields, discovery state, and isStreamActionable from KiGenerationContext. Inline onboardingConfig state, delete use_connector_config.ts, replace renderless KiGenerationEffects with direct onTaskCompleted/onTaskFailed props, and flatten bulk onboarding callback chain. Made-with: Cursor --- .../knowledge_indicators_table/index.ts | 2 +- .../ki_generation_context.tsx | 238 +++++++----------- .../knowledge_indicators_table.tsx | 27 +- .../streams_view/insights_split_button.tsx | 2 +- .../components/streams_view/streams_view.tsx | 37 ++- .../components/streams_view/translations.ts | 2 - .../hooks/use_bulk_onboarding.ts | 32 +-- .../significant_events_discovery/page.tsx | 50 ++-- .../hooks/sig_events/use_connector_config.ts | 75 ------ 9 files changed, 166 insertions(+), 299 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_connector_config.ts diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts index 25a6635263a5e..4fd71c8a8040c 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/index.ts @@ -6,4 +6,4 @@ */ export { KnowledgeIndicatorsTable } from './knowledge_indicators_table'; -export { KiGenerationProvider, useKiGeneration } from './ki_generation_context'; +export { KiGenerationProvider } from './ki_generation_context'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx index 06047eebd11a6..c6b65e323dda4 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx @@ -5,10 +5,14 @@ * 2.0. */ -import type { InferenceConnector } from '@kbn/inference-common'; import type { ListStreamDetail } from '@kbn/streams-plugin/server/routes/internal/streams/crud/route'; import type { OnboardingResult, TaskResult } from '@kbn/streams-schema'; -import { TaskStatus } from '@kbn/streams-schema'; +import { + OnboardingStep, + STREAMS_SIG_EVENTS_KI_EXTRACTION_INFERENCE_FEATURE_ID, + STREAMS_SIG_EVENTS_KI_QUERY_GENERATION_INFERENCE_FEATURE_ID, + TaskStatus, +} from '@kbn/streams-schema'; import React, { createContext, useCallback, @@ -18,19 +22,14 @@ import React, { useRef, useState, } from 'react'; -import { useConnectorConfig } from '../../../../../hooks/sig_events/use_connector_config'; +import { useInferenceFeatureConnectors } from '../../../../../hooks/sig_events/use_inference_feature_connectors'; import { useIndexPatternsConfig } from '../../../../../hooks/use_index_patterns_config'; import type { ScheduleOnboardingOptions } from '../../../../../hooks/use_onboarding_api'; import { useBulkOnboarding } from '../../hooks/use_bulk_onboarding'; import { useFetchStreams } from '../../hooks/use_fetch_streams'; import type { OnboardingConfig } from '../shared/types'; -const IN_PROGRESS_STATUSES: readonly TaskStatus[] = [ - TaskStatus.InProgress, - TaskStatus.BeingCanceled, -]; - -type StreamStatusCallback = (streamName: string, taskResult: TaskResult) => void; +const IN_PROGRESS_STATUSES = new Set([TaskStatus.InProgress, TaskStatus.BeingCanceled]); interface ConnectorState { resolvedConnectorId: string | undefined; @@ -45,18 +44,10 @@ interface KiGenerationContextValue { isGenerating: boolean; isScheduling: boolean; streamStatusMap: Record>; - generationCompletedAt: number | undefined; onboardingConfig: OnboardingConfig; setOnboardingConfig: (config: OnboardingConfig) => void; - allConnectors: InferenceConnector[]; - connectorError: Error | undefined; featuresConnectors: ConnectorState; queriesConnectors: ConnectorState; - discoveryConnectors: ConnectorState; - isConnectorCatalogUnavailable: boolean; - discoveryConnectorOverride: string | undefined; - setDiscoveryConnectorOverride: (id: string | undefined) => void; - displayDiscoveryConnectorId: string | undefined; bulkOnboardAll: (streamNames: string[]) => Promise; bulkOnboardFeaturesOnly: (streamNames: string[]) => Promise; bulkOnboardQueriesOnly: (streamNames: string[]) => Promise; @@ -65,40 +56,56 @@ interface KiGenerationContextValue { options?: ScheduleOnboardingOptions ) => Promise; cancelOnboardingTask: (streamName: string) => Promise; - isStreamActionable: (streamName: string) => boolean; - registerStatusCallback: (cb: StreamStatusCallback) => () => void; } const KiGenerationReactContext = createContext(null); -export function KiGenerationProvider({ children }: { children: React.ReactNode }) { +interface KiGenerationProviderProps { + children: React.ReactNode; + onTaskCompleted?: () => void; + onTaskFailed?: (error: string) => void; +} + +export function KiGenerationProvider({ + children, + onTaskCompleted, + onTaskFailed, +}: KiGenerationProviderProps) { const [generatingStreams, setGeneratingStreams] = useState>(new Set()); const [streamStatusMap, setStreamStatusMap] = useState< Record> >({}); - const [generationCompletedAt, setGenerationCompletedAt] = useState(undefined); - const statusCallbacksRef = useRef>(new Set()); - const prevGeneratingSizeRef = useRef(0); - // Ref gates callback forwarding without stale closures; state flag drives - // useMemo reactivity for isInitialGenerationStatusLoading. const initialStatusFetchDoneRef = useRef(false); - const [initialStatusFetchDone, setInitialStatusFetchDone] = useState(false); + // Dedup guard: filteredStreams gets a new array reference on every render + // (due to the select transform), which re-fires the status-fetch effect. + // This ref tracks already-enqueued names so only truly new streams trigger + // network calls. const enqueuedStreamNamesRef = useRef>(new Set()); const { filterStreamsByIndexPatterns } = useIndexPatternsConfig(); - const { - onboardingConfig, - setOnboardingConfig, - allConnectors, - connectorError, - featuresConnectors, - queriesConnectors, - discoveryConnectors, - isConnectorCatalogUnavailable, - discoveryConnectorOverride, - setDiscoveryConnectorOverride, - displayDiscoveryConnectorId, - } = useConnectorConfig(); + + const featuresConnectors = useInferenceFeatureConnectors( + STREAMS_SIG_EVENTS_KI_EXTRACTION_INFERENCE_FEATURE_ID + ); + const queriesConnectors = useInferenceFeatureConnectors( + STREAMS_SIG_EVENTS_KI_QUERY_GENERATION_INFERENCE_FEATURE_ID + ); + + const [onboardingConfig, setOnboardingConfig] = useState({ + steps: [OnboardingStep.FeaturesIdentification, OnboardingStep.QueriesGeneration], + connectors: {}, + }); + + useEffect(() => { + setOnboardingConfig((prev) => { + const features = prev.connectors.features ?? featuresConnectors.resolvedConnectorId; + const queries = prev.connectors.queries ?? queriesConnectors.resolvedConnectorId; + if (features === prev.connectors.features && queries === prev.connectors.queries) { + return prev; + } + return { ...prev, connectors: { features, queries } }; + }); + }, [featuresConnectors.resolvedConnectorId, queriesConnectors.resolvedConnectorId]); const streamsListFetch = useFetchStreams({ select: (result) => ({ @@ -109,38 +116,15 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } const filteredStreams = streamsListFetch.data?.streams; const isStreamsLoading = streamsListFetch.isLoading; - const registerStatusCallback = useCallback((cb: StreamStatusCallback) => { - statusCallbacksRef.current.add(cb); - return () => { - statusCallbacksRef.current.delete(cb); - }; - }, []); - - useEffect(() => { - if (prevGeneratingSizeRef.current > 0 && generatingStreams.size === 0) { - setGenerationCompletedAt(Date.now()); - } - prevGeneratingSizeRef.current = generatingStreams.size; - }, [generatingStreams]); - - const markAsGenerating = useCallback((streamNames: string[]) => { - if (streamNames.length === 0) return; - setGeneratingStreams((current) => { - const next = new Set(current); - streamNames.forEach((s) => next.add(s)); - return next; - }); - }, []); - - // Bidirectional: adds streams discovered as InProgress (e.g. on initial status - // fetch after page refresh) and removes streams that reach a terminal state. - // Callback forwarding is gated on the initial-fetch flag so initial-load - // updates don't trigger consumer side effects (like error toasts). + // Adds streams discovered as InProgress (e.g. on initial status fetch after + // page refresh) and removes streams that reach a terminal state. Callback + // forwarding is gated on the initial-fetch flag so initial-load updates + // don't trigger consumer side effects (like error toasts). const onStreamStatusUpdate = useCallback( (streamName: string, taskResult: TaskResult) => { setStreamStatusMap((current) => ({ ...current, [streamName]: taskResult })); - const isInProgress = IN_PROGRESS_STATUSES.includes(taskResult.status); + const isInProgress = IN_PROGRESS_STATUSES.has(taskResult.status); setGeneratingStreams((current) => { const has = current.has(streamName); @@ -155,10 +139,15 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } }); if (initialStatusFetchDoneRef.current) { - statusCallbacksRef.current.forEach((cb) => cb(streamName, taskResult)); + if (taskResult.status === TaskStatus.Failed) { + onTaskFailed?.(taskResult.error ?? 'Unknown error'); + } + if (taskResult.status === TaskStatus.Completed) { + onTaskCompleted?.(); + } } }, - [] + [onTaskCompleted, onTaskFailed] ); const bulkOnboarding = useBulkOnboarding({ onboardingConfig, onStreamStatusUpdate }); @@ -185,7 +174,6 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } if (hasNew) { processStatusUpdateQueue().finally(() => { initialStatusFetchDoneRef.current = true; - setInitialStatusFetchDone(true); }); } }, [filteredStreams, onboardingStatusUpdateQueue, processStatusUpdateQueue]); @@ -198,70 +186,48 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } // generating set is known. Once false, stays false — transient refetches of // the streams list must not flash the loading panel again. const isInitialGenerationStatusLoading = useMemo(() => { - if (initialStatusFetchDone) return false; + if (initialStatusFetchDoneRef.current) return false; if (isStreamsLoading || !filteredStreams) return true; return filteredStreams.some((item) => !(item.stream.name in streamStatusMap)); - }, [initialStatusFetchDone, isStreamsLoading, filteredStreams, streamStatusMap]); - - const unmarkFailedStreams = useCallback((requested: string[], succeeded: string[]) => { - if (succeeded.length === requested.length) return; - const succeededSet = new Set(succeeded); - const failed = requested.filter((s) => !succeededSet.has(s)); - if (failed.length === 0) return; - setGeneratingStreams((current) => { - const next = new Set(current); - failed.forEach((s) => next.delete(s)); - return next; - }); - }, []); + }, [isStreamsLoading, filteredStreams, streamStatusMap]); - const bulkOnboardAll = useCallback( - async (streamNames: string[]): Promise => { - markAsGenerating(streamNames); - const succeeded = await rawBulkOnboardAll(streamNames); - unmarkFailedStreams(streamNames, succeeded); - return succeeded; - }, - [markAsGenerating, rawBulkOnboardAll, unmarkFailedStreams] + const withGeneratingTracking = useCallback( + (action: (streamNames: string[]) => Promise) => + async (streamNames: string[]): Promise => { + if (streamNames.length > 0) { + setGeneratingStreams((current) => new Set([...current, ...streamNames])); + } + const succeeded = await action(streamNames); + if (succeeded.length < streamNames.length) { + const succeededSet = new Set(succeeded); + const failed = streamNames.filter((s) => !succeededSet.has(s)); + setGeneratingStreams((current) => { + const next = new Set(current); + failed.forEach((s) => next.delete(s)); + return next; + }); + } + return succeeded; + }, + [] ); - const bulkOnboardFeaturesOnly = useCallback( - async (streamNames: string[]): Promise => { - markAsGenerating(streamNames); - const succeeded = await rawBulkOnboardFeaturesOnly(streamNames); - unmarkFailedStreams(streamNames, succeeded); - return succeeded; - }, - [markAsGenerating, rawBulkOnboardFeaturesOnly, unmarkFailedStreams] + const bulkOnboardAll = useMemo( + () => withGeneratingTracking(rawBulkOnboardAll), + [withGeneratingTracking, rawBulkOnboardAll] ); - - const bulkOnboardQueriesOnly = useCallback( - async (streamNames: string[]): Promise => { - markAsGenerating(streamNames); - const succeeded = await rawBulkOnboardQueriesOnly(streamNames); - unmarkFailedStreams(streamNames, succeeded); - return succeeded; - }, - [markAsGenerating, rawBulkOnboardQueriesOnly, unmarkFailedStreams] + const bulkOnboardFeaturesOnly = useMemo( + () => withGeneratingTracking(rawBulkOnboardFeaturesOnly), + [withGeneratingTracking, rawBulkOnboardFeaturesOnly] ); - - const bulkScheduleOnboardingTask = useCallback( - async (streamNames: string[], options?: ScheduleOnboardingOptions): Promise => { - markAsGenerating(streamNames); - const succeeded = await rawBulkScheduleOnboardingTask(streamNames, options); - unmarkFailedStreams(streamNames, succeeded); - return succeeded; - }, - [markAsGenerating, rawBulkScheduleOnboardingTask, unmarkFailedStreams] + const bulkOnboardQueriesOnly = useMemo( + () => withGeneratingTracking(rawBulkOnboardQueriesOnly), + [withGeneratingTracking, rawBulkOnboardQueriesOnly] ); - - const isStreamActionable = useCallback( - (streamName: string) => { - const result = streamStatusMap[streamName]; - if (!result) return false; - return !IN_PROGRESS_STATUSES.includes(result.status); - }, - [streamStatusMap] + const bulkScheduleOnboardingTask = useCallback( + (streamNames: string[], options?: ScheduleOnboardingOptions) => + withGeneratingTracking((names) => rawBulkScheduleOnboardingTask(names, options))(streamNames), + [withGeneratingTracking, rawBulkScheduleOnboardingTask] ); const value = useMemo( @@ -274,24 +240,14 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } generatingStreamNames, isGenerating, streamStatusMap, - generationCompletedAt, onboardingConfig, setOnboardingConfig, - allConnectors, - connectorError, featuresConnectors, queriesConnectors, - discoveryConnectors, - isConnectorCatalogUnavailable, - discoveryConnectorOverride, - setDiscoveryConnectorOverride, - displayDiscoveryConnectorId, bulkOnboardAll, bulkOnboardFeaturesOnly, bulkOnboardQueriesOnly, bulkScheduleOnboardingTask, - isStreamActionable, - registerStatusCallback, }), [ bulkOnboarding.isScheduling, @@ -302,24 +258,14 @@ export function KiGenerationProvider({ children }: { children: React.ReactNode } generatingStreamNames, isGenerating, streamStatusMap, - generationCompletedAt, onboardingConfig, setOnboardingConfig, - allConnectors, - connectorError, featuresConnectors, queriesConnectors, - discoveryConnectors, - isConnectorCatalogUnavailable, - discoveryConnectorOverride, - setDiscoveryConnectorOverride, - displayDiscoveryConnectorId, bulkOnboardAll, bulkOnboardFeaturesOnly, bulkOnboardQueriesOnly, bulkScheduleOnboardingTask, - isStreamActionable, - registerStatusCallback, ] ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx index 61d48be1b9b00..862e33ac3dd7a 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx @@ -20,6 +20,7 @@ import { import { css } from '@emotion/react'; import type { KnowledgeIndicator } from '@kbn/streams-ai'; import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useAIFeatures } from '../../../../../hooks/use_ai_features'; import { AssetImage } from '../../../../asset_image'; import { LoadingPanel } from '../../../../loading_panel'; import { KnowledgeIndicatorDetailsFlyout } from '../../../stream_detail_significant_events_view/knowledge_indicator_details_flyout'; @@ -54,19 +55,21 @@ export function KnowledgeIndicatorsTable() { isGenerating, isInitialGenerationStatusLoading, isScheduling, - generationCompletedAt, onboardingConfig, setOnboardingConfig, - allConnectors, - connectorError, featuresConnectors, queriesConnectors, - isConnectorCatalogUnavailable, bulkOnboardAll, bulkOnboardFeaturesOnly, bulkOnboardQueriesOnly, } = useKiGeneration(); + const aiFeatures = useAIFeatures(); + const allConnectors = aiFeatures?.genAiConnectors?.connectors ?? []; + const connectorError = aiFeatures?.genAiConnectors?.error; + const isConnectorCatalogUnavailable = + !allConnectors.length || !!aiFeatures?.genAiConnectors?.loading || !!connectorError; + const runAndClearPicker = useCallback( async (action: (names: string[]) => Promise) => { const names = generationStreamNames; @@ -135,16 +138,18 @@ export function KnowledgeIndicatorsTable() { deleteKnowledgeIndicatorsInBulk, } = useKnowledgeIndicatorsTable(); - const lastHandledGenerationCompletedAt = useRef(undefined); + const wasGeneratingRef = useRef(false); useEffect(() => { - if ( - generationCompletedAt !== undefined && - generationCompletedAt !== lastHandledGenerationCompletedAt.current - ) { - lastHandledGenerationCompletedAt.current = generationCompletedAt; + if (isGenerating) { + wasGeneratingRef.current = true; + const id = setInterval(() => refetch(), 10_000); + return () => clearInterval(id); + } + if (wasGeneratingRef.current) { + wasGeneratingRef.current = false; refetch(); } - }, [generationCompletedAt, refetch]); + }, [isGenerating, refetch]); const columns = useKnowledgeIndicatorsColumns({ occurrencesByQueryId, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx index 0344e2fc4be67..f634162c7e85e 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx @@ -8,10 +8,10 @@ import type { InferenceConnector } from '@kbn/inference-common'; import React, { useCallback, useMemo } from 'react'; import { - CONNECTOR_LOAD_ERROR, DISCOVER_INSIGHTS_BUTTON_LABEL, DISCOVER_INSIGHTS_CONFIG_ARIA_LABEL, } from './translations'; +import { CONNECTOR_LOAD_ERROR } from '../shared/translations'; import { buildConnectorMenuItem, buildConnectorSelectionPanel, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx index a091062eb9bef..5c26db7b3d5b2 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx @@ -10,10 +10,12 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSearchBar, EuiText } from '@el import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import { TaskStatus } from '@kbn/streams-schema'; +import { STREAMS_SIG_EVENTS_DISCOVERY_INFERENCE_FEATURE_ID, TaskStatus } from '@kbn/streams-schema'; import React, { useCallback, useEffect, useState } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import type { TableRow } from './utils'; +import { useInferenceFeatureConnectors } from '../../../../../hooks/sig_events/use_inference_feature_connectors'; +import { useAIFeatures } from '../../../../../hooks/use_ai_features'; import { useKibana } from '../../../../../hooks/use_kibana'; import { useInsightsDiscoveryApi } from '../../../../../hooks/sig_events/use_insights_discovery_api'; import { useStreamsAppRouter } from '../../../../../hooks/use_streams_app_router'; @@ -32,6 +34,8 @@ import { } from './translations'; import { StreamsTreeTable } from './tree_table'; +const IN_PROGRESS_STATUSES = new Set([TaskStatus.InProgress, TaskStatus.BeingCanceled]); + const datePickerStyle = css` .euiFormControlLayout, .euiSuperDatePicker button, @@ -56,24 +60,39 @@ export function StreamsView() { isScheduling, onboardingConfig, setOnboardingConfig, - allConnectors, - connectorError, featuresConnectors, queriesConnectors, - discoveryConnectors, - isConnectorCatalogUnavailable, - discoveryConnectorOverride, - setDiscoveryConnectorOverride, - displayDiscoveryConnectorId, streamStatusMap, cancelOnboardingTask, bulkScheduleOnboardingTask, bulkOnboardAll, bulkOnboardFeaturesOnly, bulkOnboardQueriesOnly, - isStreamActionable, } = useKiGeneration(); + const discoveryConnectors = useInferenceFeatureConnectors( + STREAMS_SIG_EVENTS_DISCOVERY_INFERENCE_FEATURE_ID + ); + const aiFeatures = useAIFeatures(); + const allConnectors = aiFeatures?.genAiConnectors?.connectors ?? []; + const connectorError = aiFeatures?.genAiConnectors?.error; + const isConnectorCatalogUnavailable = + !allConnectors.length || !!aiFeatures?.genAiConnectors?.loading || !!connectorError; + + const [discoveryConnectorOverride, setDiscoveryConnectorOverride] = useState< + string | undefined + >(); + const displayDiscoveryConnectorId = + discoveryConnectorOverride ?? discoveryConnectors.resolvedConnectorId; + + const isStreamActionable = useCallback( + (streamName: string) => { + const result = streamStatusMap[streamName]; + return !!result && !IN_PROGRESS_STATUSES.has(result.status); + }, + [streamStatusMap] + ); + const [selectedStreams, setSelectedStreams] = useState([]); const router = useStreamsAppRouter(); const { scheduleInsightsDiscoveryTask, getInsightsDiscoveryTaskStatus } = diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/translations.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/translations.ts index 080b6c9f0cdd3..64c14c7126932 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/translations.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/translations.ts @@ -7,8 +7,6 @@ import { i18n } from '@kbn/i18n'; -export { CONNECTOR_LOAD_ERROR } from '../shared/translations'; - export const NAME_COLUMN_HEADER = i18n.translate('xpack.streams.streamsTreeTable.nameColumnName', { defaultMessage: 'Name', }); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts index 2c483aab25dc9..451d359bca846 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts @@ -79,34 +79,26 @@ export function useBulkOnboarding({ ); const bulkOnboardAll = useCallback( - async (streamNames: string[]): Promise => { - return bulkScheduleOnboardingTask(streamNames, onboardingConfig); - }, + (streamNames: string[]) => bulkScheduleOnboardingTask(streamNames, onboardingConfig), [bulkScheduleOnboardingTask, onboardingConfig] ); - const bulkOnboardStep = useCallback( - async (streamNames: string[], step: OnboardingStep): Promise => { - return bulkScheduleOnboardingTask(streamNames, { - steps: [step], + const bulkOnboardFeaturesOnly = useCallback( + (streamNames: string[]) => + bulkScheduleOnboardingTask(streamNames, { + steps: [OnboardingStep.FeaturesIdentification], connectors: onboardingConfig.connectors, - }); - }, + }), [bulkScheduleOnboardingTask, onboardingConfig.connectors] ); - const bulkOnboardFeaturesOnly = useCallback( - async (streamNames: string[]): Promise => { - return bulkOnboardStep(streamNames, OnboardingStep.FeaturesIdentification); - }, - [bulkOnboardStep] - ); - const bulkOnboardQueriesOnly = useCallback( - async (streamNames: string[]): Promise => { - return bulkOnboardStep(streamNames, OnboardingStep.QueriesGeneration); - }, - [bulkOnboardStep] + (streamNames: string[]) => + bulkScheduleOnboardingTask(streamNames, { + steps: [OnboardingStep.QueriesGeneration], + connectors: onboardingConfig.connectors, + }), + [bulkScheduleOnboardingTask, onboardingConfig.connectors] ); return { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx index 713fb0fdef2bb..5bdddc4334e5a 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/page.tsx @@ -15,8 +15,7 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { TaskStatus } from '@kbn/streams-schema'; -import React, { useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useIsMutating } from '@kbn/react-query'; import { useKibana } from '../../../hooks/use_kibana'; import { getFormattedError } from '../../../util/errors'; @@ -31,7 +30,6 @@ import { StreamsAppPageTemplate } from '../../streams_app_page_template'; import { KnowledgeIndicatorsTable, KiGenerationProvider, - useKiGeneration, } from './components/knowledge_indicators_table'; import { ONBOARDING_FAILURE_TITLE } from './components/streams_view/translations'; import { QueriesTable } from './components/queries_table/queries_table'; @@ -54,41 +52,17 @@ function isValidDiscoveryTab(value: string): value is DiscoveryTab { return discoveryTabs.includes(value as DiscoveryTab); } -function KiGenerationEffects({ - refreshUnbackedQueriesCount, -}: { - refreshUnbackedQueriesCount: () => void; -}) { - const { registerStatusCallback } = useKiGeneration(); - const { - core: { - notifications: { toasts }, - }, - } = useKibana(); - - useEffect(() => { - return registerStatusCallback((_streamName, taskResult) => { - if (taskResult.status === TaskStatus.Failed) { - toasts.addError(getFormattedError(new Error(taskResult.error)), { - title: ONBOARDING_FAILURE_TITLE, - }); - } - - if (taskResult.status === TaskStatus.Completed) { - refreshUnbackedQueriesCount(); - } - }); - }, [registerStatusCallback, toasts, refreshUnbackedQueriesCount]); - - return null; -} - export function SignificantEventsDiscoveryPage() { const { path: { tab }, } = useStreamsAppParams('/_discovery/{tab}'); const router = useStreamsAppRouter(); + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); const { features: { significantEventsDiscovery }, @@ -96,6 +70,15 @@ export function SignificantEventsDiscoveryPage() { const { euiTheme } = useEuiTheme(); const { count: unbackedQueriesCount, refetch } = useUnbackedQueriesCount(); + const onTaskFailed = useCallback( + (error: string) => { + toasts.addError(getFormattedError(new Error(error)), { + title: ONBOARDING_FAILURE_TITLE, + }); + }, + [toasts] + ); + const { isMemoryEnabled, isLoading: isSettingsLoading } = useDiscoverySettings(); const isPromotingQueries = useIsMutating({ mutationKey: ['promoteAll'] }) > 0; @@ -219,8 +202,7 @@ export function SignificantEventsDiscoveryPage() { } tabs={tabs} /> - - + {tab === 'streams' && } {tab === 'knowledge_indicators' && } diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_connector_config.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_connector_config.ts deleted file mode 100644 index f0bb3e8dbb156..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_connector_config.ts +++ /dev/null @@ -1,75 +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 { - OnboardingStep, - STREAMS_SIG_EVENTS_DISCOVERY_INFERENCE_FEATURE_ID, - STREAMS_SIG_EVENTS_KI_EXTRACTION_INFERENCE_FEATURE_ID, - STREAMS_SIG_EVENTS_KI_QUERY_GENERATION_INFERENCE_FEATURE_ID, -} from '@kbn/streams-schema'; -import type { InferenceConnector } from '@kbn/inference-common'; -import { useEffect, useState } from 'react'; -import type { OnboardingConfig } from '../../components/sig_events/significant_events_discovery/components/shared/types'; -import { useAIFeatures } from '../use_ai_features'; -import { useInferenceFeatureConnectors } from './use_inference_feature_connectors'; - -const EMPTY_CONNECTORS: InferenceConnector[] = []; - -export function useConnectorConfig() { - const featuresConnectors = useInferenceFeatureConnectors( - STREAMS_SIG_EVENTS_KI_EXTRACTION_INFERENCE_FEATURE_ID - ); - const queriesConnectors = useInferenceFeatureConnectors( - STREAMS_SIG_EVENTS_KI_QUERY_GENERATION_INFERENCE_FEATURE_ID - ); - const discoveryConnectors = useInferenceFeatureConnectors( - STREAMS_SIG_EVENTS_DISCOVERY_INFERENCE_FEATURE_ID - ); - - const aiFeatures = useAIFeatures(); - const genAiConnectors = aiFeatures?.genAiConnectors; - const allConnectors = genAiConnectors?.connectors ?? EMPTY_CONNECTORS; - const connectorError = genAiConnectors?.error; - const isConnectorCatalogUnavailable = - !allConnectors.length || !!genAiConnectors?.loading || !!connectorError; - - const [discoveryConnectorOverride, setDiscoveryConnectorOverride] = useState< - string | undefined - >(); - const displayDiscoveryConnectorId = - discoveryConnectorOverride ?? discoveryConnectors.resolvedConnectorId; - - const [onboardingConfig, setOnboardingConfig] = useState({ - steps: [OnboardingStep.FeaturesIdentification, OnboardingStep.QueriesGeneration], - connectors: {}, - }); - - useEffect(() => { - setOnboardingConfig((prev) => { - const features = prev.connectors.features ?? featuresConnectors.resolvedConnectorId; - const queries = prev.connectors.queries ?? queriesConnectors.resolvedConnectorId; - if (features === prev.connectors.features && queries === prev.connectors.queries) { - return prev; - } - return { ...prev, connectors: { features, queries } }; - }); - }, [featuresConnectors.resolvedConnectorId, queriesConnectors.resolvedConnectorId]); - - return { - featuresConnectors, - queriesConnectors, - discoveryConnectors, - allConnectors, - connectorError, - isConnectorCatalogUnavailable, - discoveryConnectorOverride, - setDiscoveryConnectorOverride, - displayDiscoveryConnectorId, - onboardingConfig, - setOnboardingConfig, - }; -} From df54e6a5d65f41e0f041d787535caf8393ccbbd2 Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Fri, 17 Apr 2026 11:52:24 +0200 Subject: [PATCH 08/11] fix: guard isStreamActionable against optimistic generating state and use isFetching in connector hook isStreamActionable now short-circuits false for streams already in the optimistic generatingStreamNames set, closing the race window between markAsGenerating and the first status poll. use_inference_feature_connectors returns loading: true during background refetches (isFetching) as defensive guard for keepPreviousData. Made-with: Cursor --- .../components/streams_view/streams_view.tsx | 4 +++- .../hooks/sig_events/use_inference_feature_connectors.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx index 5c26db7b3d5b2..303e63d02b3ae 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx @@ -62,6 +62,7 @@ export function StreamsView() { setOnboardingConfig, featuresConnectors, queriesConnectors, + generatingStreamNames, streamStatusMap, cancelOnboardingTask, bulkScheduleOnboardingTask, @@ -87,10 +88,11 @@ export function StreamsView() { const isStreamActionable = useCallback( (streamName: string) => { + if (generatingStreamNames.includes(streamName)) return false; const result = streamStatusMap[streamName]; return !!result && !IN_PROGRESS_STATUSES.has(result.status); }, - [streamStatusMap] + [generatingStreamNames, streamStatusMap] ); const [selectedStreams, setSelectedStreams] = useState([]); diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts index 5b69fd327b232..b9c0ccb2b13bb 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts @@ -49,7 +49,7 @@ export function useInferenceFeatureConnectors( return { resolvedConnectorId, - loading: query.isLoading, + loading: query.isLoading || query.isFetching, error: query.error ?? undefined, }; } From 2a31cdb64456869fa68cf9c13a5ea02b5cefe15b Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Fri, 17 Apr 2026 12:14:22 +0200 Subject: [PATCH 09/11] fix: use useLoadConnectors and prefer recommended connector over global default Switch back to useLoadConnectors from @kbn/inference-connectors. When no SO entry exists for a feature, prefer the first recommended connector instead of connectors[0] which is the global default prepended by the server. Made-with: Cursor --- .../use_inference_feature_connectors.ts | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts index b9c0ccb2b13bb..0513a6c938252 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_inference_feature_connectors.ts @@ -5,15 +5,9 @@ * 2.0. */ -import { useQuery } from '@kbn/react-query'; -import { - INFERENCE_CONNECTORS_INTERNAL_API_PATH, - type InferenceConnectorsApiResponseBody, -} from '@kbn/inference-common'; +import { useLoadConnectors } from '@kbn/inference-connectors'; import { useKibana } from '../use_kibana'; -const NO_DEFAULT_CONNECTOR = 'NO_DEFAULT_CONNECTOR'; - export interface UseInferenceFeatureConnectorsResult { resolvedConnectorId: string | undefined; loading: boolean; @@ -21,34 +15,32 @@ export interface UseInferenceFeatureConnectorsResult { } /** - * Resolves the connector to use for a given inference feature by calling - * the search_inference_endpoints API directly. This bypasses the - * default-prepending logic in useLoadConnectors, so we always get the - * feature-specific connector when one exists. + * Resolves the connector to use for a given inference feature. + * Delegates to useLoadConnectors from @kbn/inference-connectors + * and picks the best connector based on SO overrides vs recommended. */ export function useInferenceFeatureConnectors( featureId: string ): UseInferenceFeatureConnectorsResult { - const { core } = useKibana(); + const { + core: { + http, + notifications: { toasts }, + }, + } = useKibana(); - const query = useQuery( - ['streams-feature-connectors', featureId], - () => - core.http.get(INFERENCE_CONNECTORS_INTERNAL_API_PATH, { - query: { featureId }, - version: '1', - }), - { retry: false, keepPreviousData: true } - ); + const query = useLoadConnectors({ http, toasts, featureId }); + const connectors = query.data ?? []; - // Feature-specific connectors take priority over the full catalog. - const featureConnectors = query.data?.connectors ?? []; - const allConnectors = query.data?.allConnectors ?? []; - const raw = featureConnectors[0]?.connectorId ?? allConnectors[0]?.connectorId; - const resolvedConnectorId = raw === NO_DEFAULT_CONNECTOR ? undefined : raw; + // When an SO entry exists the API puts the configured connector first. + // Otherwise the API prepends the global default before the recommended + // ones, so we skip it and pick the first recommended connector instead. + const picked = query.soEntryFound + ? connectors[0] + : connectors.find((c) => c.isRecommended) ?? connectors[0]; return { - resolvedConnectorId, + resolvedConnectorId: picked?.id, loading: query.isLoading || query.isFetching, error: query.error ?? undefined, }; From 884f4a4cc05c05e0d8f35840596c3b26c302647b Mon Sep 17 00:00:00 2001 From: Francesco Fagnani Date: Tue, 21 Apr 2026 07:41:44 +0200 Subject: [PATCH 10/11] fix(cr): address review feedback from achyutjhunjhunwala - Counterfactual check for hidden-computed-features hint so it only shows when toggling the switch would actually reveal results. - Cap stream names in generation progress callout (show first two, summarise the rest). - Consolidate per-stream scheduling error toasts into a single summary toast with per-stream detail in the body. - Remove unused ONBOARDING_SCHEDULING_FAILURE_TITLE. Made-with: Cursor --- .../translations.ts | 21 ++++++++++++---- .../use_knowledge_indicators_table.ts | 22 +++++++++++++++-- .../components/shared/translations.ts | 9 +------ .../hooks/use_bulk_onboarding.ts | 24 +++++++++++++++---- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/translations.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/translations.ts index 60a35021201dc..d2a3414cf7bf0 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/translations.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/translations.ts @@ -119,11 +119,24 @@ export const GENERATION_IN_PROGRESS_TITLE = i18n.translate( } ); -export const getGenerationInProgressDescription = (streamNames: string[]): string => - i18n.translate('xpack.streams.knowledgeIndicators.generationInProgressDescription', { - defaultMessage: 'Generation is running for: {streams}. This may take a few minutes.', - values: { streams: streamNames.join(', ') }, +export const getGenerationInProgressDescription = (streamNames: string[]): string => { + const count = streamNames.length; + if (count <= 2) { + return i18n.translate('xpack.streams.knowledgeIndicators.generationInProgressDescriptionFew', { + defaultMessage: 'Generation is running for: {streams}. This may take a few minutes.', + values: { streams: streamNames.join(', ') }, + }); + } + return i18n.translate('xpack.streams.knowledgeIndicators.generationInProgressDescriptionMany', { + defaultMessage: + 'Generation is running for {first}, {second} and {remaining} more. This may take a few minutes.', + values: { + first: streamNames[0], + second: streamNames[1], + remaining: count - 2, + }, }); +}; export const HIDDEN_COMPUTED_FEATURES_HINT = i18n.translate( 'xpack.streams.knowledgeIndicators.hiddenComputedFeaturesHint', diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_table.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_table.ts index ed9c04851a1ad..98c9e9e664863 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_table.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_table.ts @@ -300,8 +300,26 @@ export function useKnowledgeIndicatorsTable() { const hasOnlyHiddenComputedFeatures = useMemo(() => { if (!hideComputedTypes || knowledgeIndicators.length === 0) return false; if (filteredKnowledgeIndicators.length > 0) return false; - return knowledgeIndicators.some((ki) => ki.kind === 'feature' && isComputedFeature(ki.feature)); - }, [hideComputedTypes, knowledgeIndicators, filteredKnowledgeIndicators]); + return knowledgeIndicators + .filter((ki) => + matchesKnowledgeIndicatorFilters(ki, { + statusFilter, + selectedTypes, + selectedStreams, + hideComputedTypes: false, + searchTerm: debouncedSearchTerm, + }) + ) + .some((ki) => ki.kind === 'feature' && isComputedFeature(ki.feature)); + }, [ + hideComputedTypes, + knowledgeIndicators, + filteredKnowledgeIndicators, + statusFilter, + selectedTypes, + selectedStreams, + debouncedSearchTerm, + ]); const { selectionContainsNonExcludable, isSelectionActionsDisabled } = useMemo(() => { const containsQueries = selectedKnowledgeIndicators.some((ki) => ki.kind === 'query'); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts index f10999fce6c0b..6892e71ab3174 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts @@ -76,11 +76,4 @@ export const GENERATE_QUERIES_TOOLTIP = i18n.translate( { defaultMessage: 'Runs only query generation on selected streams using the configured model.', } -); - -export const ONBOARDING_SCHEDULING_FAILURE_TITLE = i18n.translate( - 'xpack.streams.significantEventsDiscovery.streamsView.schedulingErrorTitle', - { - defaultMessage: 'Could not schedule a task to onboard stream', - } -); +); \ No newline at end of file diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts index 451d359bca846..d830ac3add13c 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/hooks/use_bulk_onboarding.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import type { OnboardingResult, TaskResult } from '@kbn/streams-schema'; import { OnboardingStep } from '@kbn/streams-schema'; import pMap from 'p-map'; @@ -13,7 +14,6 @@ import type { ScheduleOnboardingOptions } from '../../../../hooks/use_onboarding import { useOnboardingApi } from '../../../../hooks/use_onboarding_api'; import { useKibana } from '../../../../hooks/use_kibana'; import { getFormattedError } from '../../../../util/errors'; -import { ONBOARDING_SCHEDULING_FAILURE_TITLE } from '../components/shared/translations'; import type { OnboardingConfig } from '../components/shared/types'; import { useOnboardingStatusUpdateQueue } from './use_onboarding_status_update_queue'; @@ -47,6 +47,7 @@ export function useBulkOnboarding({ async (streamNames: string[], options?: ScheduleOnboardingOptions): Promise => { setIsScheduling(true); const succeeded: string[] = []; + const failures: Array<{ streamName: string; error: unknown }> = []; try { await pMap( streamNames, @@ -55,9 +56,7 @@ export function useBulkOnboarding({ await scheduleOnboardingTask(streamName, options); succeeded.push(streamName); } catch (error) { - toasts.addError(getFormattedError(error), { - title: ONBOARDING_SCHEDULING_FAILURE_TITLE, - }); + failures.push({ streamName, error }); } }, { concurrency: 10, stopOnError: false } @@ -66,6 +65,23 @@ export function useBulkOnboarding({ setIsScheduling(false); } + if (failures.length > 0) { + toasts.addError( + new Error( + failures + .map(({ streamName, error }) => `${streamName}: ${getFormattedError(error).message}`) + .join('\n') + ), + { + title: i18n.translate('xpack.streams.bulkOnboarding.schedulingErrorSummary', { + defaultMessage: + 'Failed to schedule onboarding for {count, plural, one {# stream} other {# streams}}', + values: { count: failures.length }, + }), + } + ); + } + succeeded.forEach((streamName) => { onboardingStatusUpdateQueue.add(streamName); }); From 90d96e46e396a4a519fe74d185a881555ba097e6 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 21 Apr 2026 06:37:44 +0000 Subject: [PATCH 11/11] Changes from node scripts/eslint_all_files --no-cache --fix --- .../components/shared/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts index 6892e71ab3174..1fca760238705 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts @@ -76,4 +76,4 @@ export const GENERATE_QUERIES_TOOLTIP = i18n.translate( { defaultMessage: 'Runs only query generation on selected streams using the configured model.', } -); \ No newline at end of file +);