diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/data_stream_step.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/data_stream_step.tsx index 6e20d739532c5..4e93d17adedd5 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/data_stream_step.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/data_stream_step.tsx @@ -20,7 +20,7 @@ import type { InputType } from '../../../../../../common'; import { useActions, type State } from '../../state'; import type { IntegrationSettings } from '../../types'; import { StepContentWrapper } from '../step_content_wrapper'; -import type { OnComplete } from './generation_modal'; +import type { OnComplete } from './use_generation'; import { GenerationModal } from './generation_modal'; import { SampleLogsInput } from './sample_logs_input'; import * as i18n from './translations'; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx index aefde66ed83db..21f82532dc21c 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx @@ -21,33 +21,13 @@ import { EuiText, useEuiTheme, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { css } from '@emotion/react'; -import { getLangSmithOptions } from '../../../../../common/lib/lang_smith'; -import type { ESProcessorItem } from '../../../../../../common'; -import { - type AnalyzeLogsRequestBody, - type CategorizationRequestBody, - type EcsMappingRequestBody, - type RelatedRequestBody, -} from '../../../../../../common'; -import { - runCategorizationGraph, - runEcsGraph, - runRelatedGraph, - runAnalyzeLogsGraph, -} from '../../../../../common/lib/api'; -import { useKibana } from '../../../../../common/hooks/use_kibana'; import type { State } from '../../state'; import * as i18n from './translations'; -import { useTelemetry } from '../../../telemetry'; -import type { ErrorCode } from '../../../../../../common/constants'; -export type OnComplete = (result: State['result']) => void; - -const ProgressOrder = ['ecs', 'categorization', 'related']; -type ProgressItem = (typeof ProgressOrder)[number]; +import type { OnComplete, ProgressItem } from './use_generation'; +import { ProgressOrder, useGeneration } from './use_generation'; const progressText: Record = { analyzeLogs: i18n.PROGRESS_ANALYZE_LOGS, @@ -56,165 +36,6 @@ const progressText: Record = { related: i18n.PROGRESS_RELATED_GRAPH, }; -interface UseGenerationProps { - integrationSettings: State['integrationSettings']; - connector: State['connector']; - onComplete: OnComplete; -} -export const useGeneration = ({ - integrationSettings, - connector, - onComplete, -}: UseGenerationProps) => { - const { reportGenerationComplete } = useTelemetry(); - const { http, notifications } = useKibana().services; - const [progress, setProgress] = useState(); - const [error, setError] = useState(null); - const [isRequesting, setIsRequesting] = useState(true); - - useEffect(() => { - if ( - !isRequesting || - http == null || - connector == null || - integrationSettings == null || - notifications?.toasts == null - ) { - return; - } - const generationStartedAt = Date.now(); - const abortController = new AbortController(); - const deps = { http, abortSignal: abortController.signal }; - - (async () => { - try { - let additionalProcessors: ESProcessorItem[] | undefined; - - // logSamples may be modified to JSON format if they are in different formats - // Keeping originalLogSamples for running pipeline and generating docs - const originalLogSamples = integrationSettings.logSamples; - let logSamples = integrationSettings.logSamples; - let samplesFormat = integrationSettings.samplesFormat; - - if (integrationSettings.samplesFormat === undefined) { - const analyzeLogsRequest: AnalyzeLogsRequestBody = { - packageName: integrationSettings.name ?? '', - dataStreamName: integrationSettings.dataStreamName ?? '', - logSamples: integrationSettings.logSamples ?? [], - connectorId: connector.id, - langSmithOptions: getLangSmithOptions(), - }; - - setProgress('analyzeLogs'); - const analyzeLogsResult = await runAnalyzeLogsGraph(analyzeLogsRequest, deps); - if (abortController.signal.aborted) return; - if (isEmpty(analyzeLogsResult?.results)) { - setError('No results from Analyze Logs Graph'); - return; - } - logSamples = analyzeLogsResult.results.parsedSamples; - samplesFormat = analyzeLogsResult.results.samplesFormat; - additionalProcessors = analyzeLogsResult.additionalProcessors; - } - - const ecsRequest: EcsMappingRequestBody = { - packageName: integrationSettings.name ?? '', - dataStreamName: integrationSettings.dataStreamName ?? '', - rawSamples: logSamples ?? [], - samplesFormat: samplesFormat ?? { name: 'json' }, - additionalProcessors: additionalProcessors ?? [], - connectorId: connector.id, - langSmithOptions: getLangSmithOptions(), - }; - - setProgress('ecs'); - const ecsGraphResult = await runEcsGraph(ecsRequest, deps); - if (abortController.signal.aborted) return; - if (isEmpty(ecsGraphResult?.results)) { - setError('No results from ECS graph'); - return; - } - const categorizationRequest: CategorizationRequestBody = { - ...ecsRequest, - rawSamples: originalLogSamples ?? [], - samplesFormat: samplesFormat ?? { name: 'json' }, - currentPipeline: ecsGraphResult.results.pipeline, - }; - - setProgress('categorization'); - const categorizationResult = await runCategorizationGraph(categorizationRequest, deps); - if (abortController.signal.aborted) return; - const relatedRequest: RelatedRequestBody = { - ...categorizationRequest, - currentPipeline: categorizationResult.results.pipeline, - }; - - setProgress('related'); - const relatedGraphResult = await runRelatedGraph(relatedRequest, deps); - if (abortController.signal.aborted) return; - - if (isEmpty(relatedGraphResult?.results)) { - throw new Error('Results not found in response'); - } - - reportGenerationComplete({ - connector, - integrationSettings, - durationMs: Date.now() - generationStartedAt, - }); - - const result = { - pipeline: relatedGraphResult.results.pipeline, - docs: relatedGraphResult.results.docs, - samplesFormat, - }; - - onComplete(result); - } catch (e) { - if (abortController.signal.aborted) return; - const originalErrorMessage = `${e.message}${ - e.body ? ` (${e.body.statusCode}): ${e.body.message}` : '' - }`; - - reportGenerationComplete({ - connector, - integrationSettings, - durationMs: Date.now() - generationStartedAt, - error: originalErrorMessage, - }); - - let errorMessage = originalErrorMessage; - const errorCode = e.body?.attributes?.errorCode as ErrorCode | undefined; - if (errorCode != null) { - errorMessage = i18n.ERROR_TRANSLATION[errorCode]; - } - setError(errorMessage); - } finally { - setIsRequesting(false); - } - })(); - return () => { - abortController.abort(); - }; - }, [ - isRequesting, - onComplete, - setProgress, - connector, - http, - integrationSettings, - reportGenerationComplete, - notifications?.toasts, - ]); - - const retry = useCallback(() => { - setError(null); - setIsRequesting(true); - }, []); - - return { progress, error, retry }; -}; - const useModalCss = () => { const { euiTheme } = useEuiTheme(); return { diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/use_generation.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/use_generation.tsx new file mode 100644 index 0000000000000..d062a0ff8b836 --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/use_generation.tsx @@ -0,0 +1,203 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import { useCallback, useEffect, useState } from 'react'; +import type { HttpSetup } from '@kbn/core-http-browser'; +import { getLangSmithOptions } from '../../../../../common/lib/lang_smith'; +import type { Docs, ESProcessorItem, Pipeline, SamplesFormat } from '../../../../../../common'; +import { + type AnalyzeLogsRequestBody, + type CategorizationRequestBody, + type EcsMappingRequestBody, + type RelatedRequestBody, +} from '../../../../../../common'; +import { + runCategorizationGraph, + runEcsGraph, + runRelatedGraph, + runAnalyzeLogsGraph, +} from '../../../../../common/lib/api'; +import { useKibana } from '../../../../../common/hooks/use_kibana'; +import type { State } from '../../state'; +import * as i18n from './translations'; +import { useTelemetry } from '../../../telemetry'; +import type { ErrorCode } from '../../../../../../common/constants'; +import type { AIConnector, IntegrationSettings } from '../../types'; + +export type OnComplete = (result: State['result']) => void; +export const ProgressOrder = ['analyzeLogs', 'ecs', 'categorization', 'related'] as const; +export type ProgressItem = (typeof ProgressOrder)[number]; + +interface UseGenerationProps { + integrationSettings: State['integrationSettings']; + connector: State['connector']; + onComplete: OnComplete; +} + +interface RunGenerationProps { + integrationSettings: IntegrationSettings; + connector: AIConnector; + deps: { http: HttpSetup; abortSignal: AbortSignal }; + setProgress: (progress: ProgressItem) => void; +} + +interface GenerationResults { + pipeline: Pipeline; + docs: Docs; + samplesFormat?: SamplesFormat; +} + +export const useGeneration = ({ + integrationSettings, + connector, + onComplete, +}: UseGenerationProps) => { + const { reportGenerationComplete } = useTelemetry(); + const { http, notifications } = useKibana().services; + const [progress, setProgress] = useState(); + const [error, setError] = useState(null); + const [isRequesting, setIsRequesting] = useState(true); + + useEffect(() => { + if ( + !isRequesting || + http == null || + connector == null || + integrationSettings == null || + notifications?.toasts == null + ) { + return; + } + const generationStartedAt = Date.now(); + const abortController = new AbortController(); + const deps = { http, abortSignal: abortController.signal }; + + (async () => { + try { + const result = await runGeneration({ integrationSettings, connector, deps, setProgress }); + const durationMs = Date.now() - generationStartedAt; + reportGenerationComplete({ connector, integrationSettings, durationMs }); + onComplete(result); + } catch (e) { + if (abortController.signal.aborted) return; + const originalErrorMessage = `${e.message}${ + e.body ? ` (${e.body.statusCode}): ${e.body.message}` : '' + }`; + + reportGenerationComplete({ + connector, + integrationSettings, + durationMs: Date.now() - generationStartedAt, + error: originalErrorMessage, + }); + + let errorMessage = originalErrorMessage; + const errorCode = e.body?.attributes?.errorCode as ErrorCode | undefined; + if (errorCode != null) { + errorMessage = i18n.ERROR_TRANSLATION[errorCode]; + } + setError(errorMessage); + } finally { + setIsRequesting(false); + } + })(); + return () => { + abortController.abort(); + }; + }, [ + isRequesting, + onComplete, + setProgress, + connector, + http, + integrationSettings, + reportGenerationComplete, + notifications?.toasts, + ]); + + const retry = useCallback(() => { + setError(null); + setIsRequesting(true); + }, []); + + return { progress, error, retry }; +}; + +async function runGeneration({ + integrationSettings, + connector, + deps, + setProgress, +}: RunGenerationProps): Promise { + let additionalProcessors: ESProcessorItem[] | undefined; + // logSamples may be modified to JSON format if they are in different formats + // Keeping originalLogSamples for running pipeline and generating docs + const originalLogSamples = integrationSettings.logSamples; + let logSamples = integrationSettings.logSamples; + let samplesFormat: SamplesFormat | undefined = integrationSettings.samplesFormat; + + if (integrationSettings.samplesFormat === undefined) { + const analyzeLogsRequest: AnalyzeLogsRequestBody = { + packageName: integrationSettings.name ?? '', + dataStreamName: integrationSettings.dataStreamName ?? '', + logSamples: integrationSettings.logSamples ?? [], + connectorId: connector.id, + langSmithOptions: getLangSmithOptions(), + }; + + setProgress('analyzeLogs'); + const analyzeLogsResult = await runAnalyzeLogsGraph(analyzeLogsRequest, deps); + if (isEmpty(analyzeLogsResult?.results)) { + throw new Error('No results from Analyze Logs Graph'); + } + logSamples = analyzeLogsResult.results.parsedSamples; + samplesFormat = analyzeLogsResult.results.samplesFormat; + additionalProcessors = analyzeLogsResult.additionalProcessors; + } + + const ecsRequest: EcsMappingRequestBody = { + packageName: integrationSettings.name ?? '', + dataStreamName: integrationSettings.dataStreamName ?? '', + rawSamples: logSamples ?? [], + samplesFormat: samplesFormat ?? { name: 'json' }, + additionalProcessors: additionalProcessors ?? [], + connectorId: connector.id, + langSmithOptions: getLangSmithOptions(), + }; + + setProgress('ecs'); + const ecsGraphResult = await runEcsGraph(ecsRequest, deps); + if (isEmpty(ecsGraphResult?.results)) { + throw new Error('No results from ECS graph'); + } + const categorizationRequest: CategorizationRequestBody = { + ...ecsRequest, + rawSamples: originalLogSamples ?? [], + samplesFormat: samplesFormat ?? { name: 'json' }, + currentPipeline: ecsGraphResult.results.pipeline, + }; + + setProgress('categorization'); + const categorizationResult = await runCategorizationGraph(categorizationRequest, deps); + const relatedRequest: RelatedRequestBody = { + ...categorizationRequest, + currentPipeline: categorizationResult.results.pipeline, + }; + + setProgress('related'); + const relatedGraphResult = await runRelatedGraph(relatedRequest, deps); + if (isEmpty(relatedGraphResult?.results)) { + throw new Error('Results not found in response'); + } + + return { + pipeline: relatedGraphResult.results.pipeline, + docs: relatedGraphResult.results.docs, + samplesFormat, + }; +}