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..c6b65e323dda4 --- /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,283 @@ +/* + * 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 { ListStreamDetail } from '@kbn/streams-plugin/server/routes/internal/streams/crud/route'; +import type { OnboardingResult, TaskResult } 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, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +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 = new Set([TaskStatus.InProgress, TaskStatus.BeingCanceled]); + +interface ConnectorState { + resolvedConnectorId: string | undefined; + loading: boolean; +} + +interface KiGenerationContextValue { + filteredStreams: ListStreamDetail[] | undefined; + isStreamsLoading: boolean; + isInitialGenerationStatusLoading: boolean; + generatingStreamNames: string[]; + isGenerating: boolean; + isScheduling: boolean; + streamStatusMap: Record>; + onboardingConfig: OnboardingConfig; + setOnboardingConfig: (config: OnboardingConfig) => void; + featuresConnectors: ConnectorState; + queriesConnectors: ConnectorState; + bulkOnboardAll: (streamNames: string[]) => Promise; + bulkOnboardFeaturesOnly: (streamNames: string[]) => Promise; + bulkOnboardQueriesOnly: (streamNames: string[]) => Promise; + bulkScheduleOnboardingTask: ( + streamNames: string[], + options?: ScheduleOnboardingOptions + ) => Promise; + cancelOnboardingTask: (streamName: string) => Promise; +} + +const KiGenerationReactContext = createContext(null); + +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 initialStatusFetchDoneRef = useRef(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 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) => ({ + ...result, + streams: filterStreamsByIndexPatterns(result.streams), + }), + }); + const filteredStreams = streamsListFetch.data?.streams; + const isStreamsLoading = streamsListFetch.isLoading; + + // 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.has(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) { + if (taskResult.status === TaskStatus.Failed) { + onTaskFailed?.(taskResult.error ?? 'Unknown error'); + } + if (taskResult.status === TaskStatus.Completed) { + onTaskCompleted?.(); + } + } + }, + [onTaskCompleted, onTaskFailed] + ); + + const bulkOnboarding = useBulkOnboarding({ onboardingConfig, onStreamStatusUpdate }); + const { + onboardingStatusUpdateQueue, + processStatusUpdateQueue, + bulkOnboardAll: rawBulkOnboardAll, + bulkOnboardFeaturesOnly: rawBulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly: rawBulkOnboardQueriesOnly, + bulkScheduleOnboardingTask: rawBulkScheduleOnboardingTask, + } = bulkOnboarding; + + useEffect(() => { + if (!filteredStreams) return; + + let hasNew = false; + filteredStreams.forEach((item) => { + 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; + }); + } + }, [filteredStreams, onboardingStatusUpdateQueue, processStatusUpdateQueue]); + + 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]); + + 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 bulkOnboardAll = useMemo( + () => withGeneratingTracking(rawBulkOnboardAll), + [withGeneratingTracking, rawBulkOnboardAll] + ); + const bulkOnboardFeaturesOnly = useMemo( + () => withGeneratingTracking(rawBulkOnboardFeaturesOnly), + [withGeneratingTracking, rawBulkOnboardFeaturesOnly] + ); + const bulkOnboardQueriesOnly = useMemo( + () => withGeneratingTracking(rawBulkOnboardQueriesOnly), + [withGeneratingTracking, rawBulkOnboardQueriesOnly] + ); + const bulkScheduleOnboardingTask = useCallback( + (streamNames: string[], options?: ScheduleOnboardingOptions) => + withGeneratingTracking((names) => rawBulkScheduleOnboardingTask(names, options))(streamNames), + [withGeneratingTracking, rawBulkScheduleOnboardingTask] + ); + + const value = useMemo( + () => ({ + isScheduling: bulkOnboarding.isScheduling, + cancelOnboardingTask: bulkOnboarding.cancelOnboardingTask, + filteredStreams, + isStreamsLoading, + isInitialGenerationStatusLoading, + generatingStreamNames, + isGenerating, + streamStatusMap, + onboardingConfig, + setOnboardingConfig, + featuresConnectors, + queriesConnectors, + bulkOnboardAll, + bulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly, + bulkScheduleOnboardingTask, + }), + [ + bulkOnboarding.isScheduling, + bulkOnboarding.cancelOnboardingTask, + filteredStreams, + isStreamsLoading, + isInitialGenerationStatusLoading, + generatingStreamNames, + isGenerating, + streamStatusMap, + onboardingConfig, + setOnboardingConfig, + featuresConnectors, + queriesConnectors, + bulkOnboardAll, + bulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly, + bulkScheduleOnboardingTask, + ] + ); + + 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 e94e1e6f1aaae..d215aff32bb4e 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 @@ -7,13 +7,13 @@ import { EuiButton, - EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiInMemoryTable, + EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiText, @@ -23,7 +23,7 @@ import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { KnowledgeIndicator } from '@kbn/streams-ai'; import { useMutation } from '@kbn/react-query'; -import React from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { HIGH_SEVERITY_THRESHOLD, useUnbackedQueriesCount, @@ -32,12 +32,15 @@ import { useQueriesApi, type PromoteResult } from '../../../../../hooks/sig_even import { useInvalidatePromoteRelatedQueries } from '../../../../../hooks/sig_events/use_invalidate_promote_queries'; import { getFormattedError } from '../../../../../util/errors'; import { useKibana } from '../../../../../hooks/use_kibana'; -import { useStreamsAppRouter } from '../../../../../hooks/use_streams_app_router'; +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'; 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'; @@ -46,8 +49,10 @@ 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, CREATE_RULES_BUTTON, getRuleCountLabel, PROMOTE_ALL_ERROR_TOAST_TITLE, @@ -55,7 +60,6 @@ import { import { getPromoteAllSuccessToast } from '../queries_table/translations'; export function KnowledgeIndicatorsTable() { - const router = useStreamsAppRouter(); const { euiTheme } = useEuiTheme(); const { core: { @@ -85,11 +89,65 @@ export function KnowledgeIndicatorsTable() { }, }); + const [generationStreamNames, setGenerationStreamNames] = useState([]); + + const { + filteredStreams, + isStreamsLoading, + generatingStreamNames, + isGenerating, + isInitialGenerationStatusLoading, + isScheduling, + onboardingConfig, + setOnboardingConfig, + featuresConnectors, + queriesConnectors, + 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; + setGenerationStreamNames([]); + await action(names); + }, + [generationStreamNames] + ); + + const onRunGeneration = useCallback( + async () => runAndClearPicker(bulkOnboardAll), + [runAndClearPicker, bulkOnboardAll] + ); + const onRunFeaturesOnly = useCallback( + async () => runAndClearPicker(bulkOnboardFeaturesOnly), + [runAndClearPicker, bulkOnboardFeaturesOnly] + ); + const onRunQueriesOnly = useCallback( + async () => runAndClearPicker(bulkOnboardQueriesOnly), + [runAndClearPicker, bulkOnboardQueriesOnly] + ); + + const isRunDisabled = + generationStreamNames.length === 0 || + isConnectorCatalogUnavailable || + featuresConnectors.loading || + queriesConnectors.loading || + isScheduling; + const { knowledgeIndicators, occurrencesByQueryId, isLoading, isEmpty, + refetch, filteredKnowledgeIndicators, selectedKnowledgeIndicator, selectedKnowledgeIndicatorId, @@ -105,6 +163,7 @@ export function KnowledgeIndicatorsTable() { selectionContainsNonExcludable, hasPromotableSelected, isSelectionActionsDisabled, + hasOnlyHiddenComputedFeatures, tableSearchValue, debouncedSearchTerm, statusFilter, @@ -125,6 +184,19 @@ export function KnowledgeIndicatorsTable() { handleBulkPromote, } = useKnowledgeIndicatorsTable(); + const wasGeneratingRef = useRef(false); + useEffect(() => { + if (isGenerating) { + wasGeneratingRef.current = true; + const id = setInterval(() => refetch(), 10_000); + return () => clearInterval(id); + } + if (wasGeneratingRef.current) { + wasGeneratingRef.current = false; + refetch(); + } + }, [isGenerating, refetch]); + const columns = useKnowledgeIndicatorsColumns({ occurrencesByQueryId, selectedKnowledgeIndicatorId, @@ -132,11 +204,58 @@ export function KnowledgeIndicatorsTable() { setKnowledgeIndicatorsToDelete, }); - if (isLoading) { + const generationRow = ( + + + + + + + + + ); + + const generationProgressCallout = isGenerating ? ( + <> + + +

{getGenerationInProgressDescription(generatingStreamNames)}

+
+ + ) : null; + + if (knowledgeIndicators.length === 0 && (isLoading || isInitialGenerationStatusLoading)) { return ; } - if (isEmpty) { + if (isEmpty && !isGenerating) { return ( } title={

{EMPTY_STATE_TITLE}

} body={

{EMPTY_STATE_DESCRIPTION}

} - actions={ - - {EMPTY_STATE_GO_TO_STREAMS} - - } + actions={generationRow} /> ); } @@ -196,6 +311,9 @@ export function KnowledgeIndicatorsTable() { )} + {generationRow} + {generationProgressCallout} + + {hasOnlyHiddenComputedFeatures && ( + <> + + + + )} { + 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', + { + 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 aa7ed752515c1..d02d400db7bca 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 @@ -316,6 +316,30 @@ export function useKnowledgeIndicatorsTable() { }, }); + const hasOnlyHiddenComputedFeatures = useMemo(() => { + if (!hideComputedTypes || knowledgeIndicators.length === 0) return false; + if (filteredKnowledgeIndicators.length > 0) return false; + 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 handleBulkPromote = useCallback(() => { const queryIds = selectedKnowledgeIndicators.flatMap((ki) => ki.kind === 'query' && !ki.rule.backed ? [ki.query.id] : [] @@ -351,6 +375,7 @@ export function useKnowledgeIndicatorsTable() { occurrencesByQueryId, isLoading, isEmpty, + refetch, filteredKnowledgeIndicators, selectedKnowledgeIndicator, selectedKnowledgeIndicatorId, @@ -366,6 +391,7 @@ export function useKnowledgeIndicatorsTable() { selectionContainsNonExcludable, hasPromotableSelected, isSelectionActionsDisabled, + hasOnlyHiddenComputedFeatures, tableSearchValue, debouncedSearchTerm, statusFilter, 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/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..bf7e86c53c4a2 --- /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,85 @@ +/* + * 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 type { ListStreamDetail } from '@kbn/streams-plugin/server/routes/internal/streams/crud/route'; +import React, { useCallback, useMemo } from 'react'; + +interface StreamPickerProps { + streams: ListStreamDetail[] | undefined; + isStreamsLoading: boolean; + selectedStreamNames: string[]; + onSelectedStreamNamesChange: (streamNames: string[]) => void; + excludedStreamNames?: string[]; + isDisabled?: boolean; + fullWidth?: boolean; +} + +export const StreamPicker = ({ + streams, + isStreamsLoading, + selectedStreamNames, + onSelectedStreamNamesChange, + excludedStreamNames, + isDisabled, + fullWidth, +}: StreamPickerProps) => { + const excludedSet = useMemo(() => new Set(excludedStreamNames ?? []), [excludedStreamNames]); + + const options = useMemo>>( + () => + (streams ?? []) + .filter((s) => !excludedSet.has(s.stream.name)) + .map((s) => ({ + label: s.stream.name, + key: s.stream.name, + })), + [streams, 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/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..1fca760238705 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/shared/translations.ts @@ -0,0 +1,79 @@ +/* + * 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.', + } +); 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/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..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,13 +8,16 @@ 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 { buildConnectorMenuItem, buildConnectorSelectionPanel } from './context_menu_helpers'; -import { ContextMenuSplitButton } from './context_menu_split_button'; -import type { MenuHelpers } from './context_menu_split_button'; +import { CONNECTOR_LOAD_ERROR } from '../shared/translations'; +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..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 @@ -10,36 +10,31 @@ 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 { 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 { useIndexPatternsConfig } from '../../../../../hooks/use_index_patterns_config'; +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 { 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, INSIGHTS_COMPLETE_TOAST_VIEW_BUTTON, 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 IN_PROGRESS_STATUSES = new Set([TaskStatus.InProgress, TaskStatus.BeingCanceled]); const datePickerStyle = css` .euiFormControlLayout, @@ -49,54 +44,59 @@ const datePickerStyle = css` } `; -interface StreamsViewProps { - refreshUnbackedQueriesCount: () => void; -} - -export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { +export function StreamsView() { const { core, core: { 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, + filteredStreams, + isStreamsLoading, + isScheduling, onboardingConfig, setOnboardingConfig, - } = useConnectorConfig(); + featuresConnectors, + queriesConnectors, + generatingStreamNames, + streamStatusMap, + cancelOnboardingTask, + bulkScheduleOnboardingTask, + bulkOnboardAll, + bulkOnboardFeaturesOnly, + bulkOnboardQueriesOnly, + } = 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 streamsListFetch = useFetchStreams({ - select: (result) => { - return { - ...result, - /** - * Significant events discovery works with streams that match the configured index patterns. - */ - streams: filterStreamsByIndexPatterns(result.streams), - }; + const [discoveryConnectorOverride, setDiscoveryConnectorOverride] = useState< + string | undefined + >(); + const displayDiscoveryConnectorId = + discoveryConnectorOverride ?? discoveryConnectors.resolvedConnectorId; + + const isStreamActionable = useCallback( + (streamName: string) => { + if (generatingStreamNames.includes(streamName)) return false; + const result = streamStatusMap[streamName]; + return !!result && !IN_PROGRESS_STATUSES.has(result.status); }, - }); + [generatingStreamNames, streamStatusMap] + ); 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 +129,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 +175,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 +187,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]); - - const onBulkOnboardStep = useCallback( - async (step: OnboardingStep) => { - const streamList = getActionableStreamNames(); - setSelectedStreams([]); - await bulkScheduleOnboardingTask(streamList, { - steps: [step], - connectors: onboardingConfig.connectors, - }); - }, - [getActionableStreamNames, bulkScheduleOnboardingTask, onboardingConfig.connectors] - ); + await bulkOnboardAll(streamList); + }, [getActionableStreamNames, bulkOnboardAll]); - 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]); @@ -331,9 +245,11 @@ export function StreamsView({ refreshUnbackedQueriesCount }: StreamsViewProps) { selectedStreams.length === 0 || isConnectorCatalogUnavailable || featuresConnectors.loading || - queriesConnectors.loading + queriesConnectors.loading || + isScheduling } isConfigDisabled={selectedStreams.length === 0} + isLoading={isScheduling} /> @@ -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): Promise => { + setIsScheduling(true); + const succeeded: string[] = []; + const failures: Array<{ streamName: string; error: unknown }> = []; + try { + await pMap( + streamNames, + async (streamName) => { + try { + await scheduleOnboardingTask(streamName, options); + succeeded.push(streamName); + } catch (error) { + failures.push({ streamName, error }); + } + }, + { concurrency: 10, stopOnError: false } + ); + } finally { + 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); + }); + if (succeeded.length > 0) { + processStatusUpdateQueue(); + } + + return succeeded; + }, + [scheduleOnboardingTask, toasts, onboardingStatusUpdateQueue, processStatusUpdateQueue] + ); + + const bulkOnboardAll = useCallback( + (streamNames: string[]) => bulkScheduleOnboardingTask(streamNames, onboardingConfig), + [bulkScheduleOnboardingTask, onboardingConfig] + ); + + const bulkOnboardFeaturesOnly = useCallback( + (streamNames: string[]) => + bulkScheduleOnboardingTask(streamNames, { + steps: [OnboardingStep.FeaturesIdentification], + connectors: onboardingConfig.connectors, + }), + [bulkScheduleOnboardingTask, onboardingConfig.connectors] + ); + + const bulkOnboardQueriesOnly = useCallback( + (streamNames: string[]) => + bulkScheduleOnboardingTask(streamNames, { + steps: [OnboardingStep.QueriesGeneration], + connectors: onboardingConfig.connectors, + }), + [bulkScheduleOnboardingTask, onboardingConfig.connectors] + ); + + 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/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 38244b6648f99..fc3ddbada44e7 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[]; @@ -28,7 +27,6 @@ export function useFetchStreams( }, }, } = useKibana(); - const { timeState } = useTimefilter(); const showFetchErrorToast = useFetchErrorToast(); const fetchStreams = async ({ signal }: QueryFunctionContext): Promise => { @@ -36,7 +34,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 809702411334e..342166fe80543 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,10 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import React, { 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'; import { useStreamsAppBreadcrumbs } from '../../../hooks/use_streams_app_breadcrumbs'; import { useStreamsAppParams } from '../../../hooks/use_streams_app_params'; import { useStreamsAppRouter } from '../../../hooks/use_streams_app_router'; @@ -25,7 +27,11 @@ 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 { 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'; @@ -52,6 +58,11 @@ export function SignificantEventsDiscoveryPage() { } = useStreamsAppParams('/_discovery/{tab}'); const router = useStreamsAppRouter(); + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); const { features: { significantEventsDiscovery }, @@ -59,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; @@ -182,14 +202,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' && } + + ); } 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 967a8c07d7df7..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/streams_view/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, - }; -} 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..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 @@ -14,21 +14,34 @@ export interface UseInferenceFeatureConnectorsResult { error: Error | undefined; } +/** + * 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 = useLoadConnectors({ http, toasts, featureId }); + const connectors = query.data ?? []; - const query = useLoadConnectors({ - http: core.http, - toasts: core.notifications.toasts, - featureId, - settings: core.settings, - }); + // 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: query.data?.[0]?.id, - loading: query.isLoading, + resolvedConnectorId: picked?.id, + loading: query.isLoading || query.isFetching, error: query.error ?? undefined, }; }