From f246c4fee24388525e6aee5fc6ee5cd29d2dd1f7 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 16 Sep 2019 17:16:31 +0100 Subject: [PATCH] [Logs UI] Enhance analysis setup flow (#44990) (#45777) * Enhance setup with a steps style flow --- ...ml_cleanup_everything.ts => ml_cleanup.ts} | 20 +- .../containers/logs/log_analysis/index.ts | 1 + .../log_analysis/log_analysis_cleanup.tsx | 14 +- .../logs/log_analysis/log_analysis_jobs.tsx | 150 ++++--------- .../log_analysis/log_analysis_setup_state.tsx | 30 +++ .../log_analysis_status_state.tsx | 199 ++++++++++++++++++ .../pages/logs/analysis/page_content.tsx | 48 ++--- .../logs/analysis/page_setup_content.tsx | 97 ++------- .../analysis_setup_timerange_form.tsx | 43 ++-- .../{ => setup}/create_ml_jobs_button.tsx | 7 +- .../pages/logs/analysis/setup/steps/index.tsx | 74 +++++++ .../setup/steps/initial_configuration.tsx | 62 ++++++ .../analysis/setup/steps/setup_process.tsx | 86 ++++++++ 13 files changed, 572 insertions(+), 259 deletions(-) rename x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/{ml_cleanup_everything.ts => ml_cleanup.ts} (62%) create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx rename x-pack/legacy/plugins/infra/public/pages/logs/analysis/{ => setup}/analysis_setup_timerange_form.tsx (75%) rename x-pack/legacy/plugins/infra/public/pages/logs/analysis/{ => setup}/create_ml_jobs_button.tsx (75%) create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/index.tsx create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/initial_configuration.tsx create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/setup_process.tsx diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup_everything.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts similarity index 62% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup_everything.ts rename to x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts index c85aadb242a80..2bb0d1d532967 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup_everything.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts @@ -6,9 +6,9 @@ import * as rt from 'io-ts'; import { kfetch } from 'ui/kfetch'; -import { getJobId } from '../../../../../common/log_analysis'; +import { getJobId, getDatafeedId } from '../../../../../common/log_analysis'; -export const callCleanupMLResources = async (spaceId: string, sourceId: string) => { +export const callDeleteJobs = async (spaceId: string, sourceId: string) => { // NOTE: Deleting the jobs via this API will delete the datafeeds at the same time const deleteJobsResponse = await kfetch({ method: 'POST', @@ -20,15 +20,19 @@ export const callCleanupMLResources = async (spaceId: string, sourceId: string) ), }); - // If for some reason we do need to delete datafeeds - // const deleteLogRateDatafeedResponse = await kfetch({ - // method: 'DELETE', - // pathname: `/api/ml/datafeeds/${getDatafeedId(spaceId, sourceId, 'log-entry-rate')}`, - // }); - return deleteJobsResponse; }; +export const callStopDatafeed = async (spaceId: string, sourceId: string) => { + // Stop datafeed due to https://github.com/elastic/kibana/issues/44652 + const stopDatafeedResponse = await kfetch({ + method: 'POST', + pathname: `/api/ml/datafeeds/${getDatafeedId(spaceId, sourceId, 'log-entry-rate')}/_stop`, + }); + + return stopDatafeedResponse; +}; + export const deleteJobsRequestPayloadRT = rt.type({ jobIds: rt.array(rt.string), }); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts index d087a5ab89104..cbe3b2ef1e9b8 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts @@ -9,3 +9,4 @@ export * from './log_analysis_cleanup'; export * from './log_analysis_jobs'; export * from './log_analysis_results'; export * from './log_analysis_results_url_state'; +export * from './log_analysis_status_state'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx index ff17914a78aea..c305b5e307ed5 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx @@ -7,7 +7,7 @@ import createContainer from 'constate-latest'; import { useMemo } from 'react'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callCleanupMLResources } from './api/ml_cleanup_everything'; +import { callDeleteJobs, callStopDatafeed } from './api/ml_cleanup'; export const useLogAnalysisCleanup = ({ sourceId, @@ -20,7 +20,17 @@ export const useLogAnalysisCleanup = ({ { cancelPreviousOn: 'resolution', createPromise: async () => { - return await callCleanupMLResources(spaceId, sourceId); + try { + await callStopDatafeed(spaceId, sourceId); + } catch (err) { + // Datefeed has been deleted / doesn't exist, proceed with deleting jobs anyway + if (err && err.res && err.res.status === 404) { + return await callDeleteJobs(spaceId, sourceId); + } else { + throw err; + } + } + return await callDeleteJobs(spaceId, sourceId); }, }, [spaceId, sourceId] diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx index c380160b08d69..a1c96f330f06b 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx @@ -5,22 +5,13 @@ */ import createContainer from 'constate-latest'; -import { useEffect, useMemo, useState } from 'react'; - -import { bucketSpan, getDatafeedId, getJobId, JobType } from '../../../../common/log_analysis'; +import { useEffect, useMemo, useCallback } from 'react'; +import { bucketSpan } from '../../../../common/log_analysis'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callJobsSummaryAPI, FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api'; +import { callJobsSummaryAPI } from './api/ml_get_jobs_summary_api'; import { callSetupMlModuleAPI, SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; - -// combines and abstracts job and datafeed status -type JobStatus = - | 'unknown' - | 'missing' - | 'initializing' - | 'stopped' - | 'started' - | 'finished' - | 'failed'; +import { useLogAnalysisCleanup } from './log_analysis_cleanup'; +import { useStatusState } from './log_analysis_status_state'; export const useLogAnalysisJobs = ({ indexPattern, @@ -33,19 +24,14 @@ export const useLogAnalysisJobs = ({ spaceId: string; timeField: string; }) => { - const [jobStatus, setJobStatus] = useState>({ - 'log-entry-rate': 'unknown', - }); - const [hasCompletedSetup, setHasCompletedSetup] = useState(false); + const { cleanupMLResources } = useLogAnalysisCleanup({ sourceId, spaceId }); + const [statusState, dispatch] = useStatusState(); const [setupMlModuleRequest, setupMlModule] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: async (start, end) => { - setJobStatus(currentJobStatus => ({ - ...currentJobStatus, - 'log-entry-rate': 'initializing', - })); + dispatch({ type: 'startedSetup' }); return await callSetupMlModuleAPI( start, end, @@ -57,46 +43,27 @@ export const useLogAnalysisJobs = ({ ); }, onResolve: ({ datafeeds, jobs }: SetupMlModuleResponsePayload) => { - setJobStatus(currentJobStatus => ({ - ...currentJobStatus, - 'log-entry-rate': - hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, 'log-entry-rate'))(jobs) && - hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, 'log-entry-rate'))( - datafeeds - ) - ? 'started' - : 'failed', - })); - - setHasCompletedSetup(true); + dispatch({ type: 'finishedSetup', datafeeds, jobs, spaceId, sourceId }); }, onReject: () => { - setJobStatus(currentJobStatus => ({ - ...currentJobStatus, - 'log-entry-rate': 'failed', - })); + dispatch({ type: 'failedSetup' }); }, }, - [indexPattern, spaceId, sourceId] + [indexPattern, spaceId, sourceId, timeField, bucketSpan] ); const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: async () => { + dispatch({ type: 'fetchingJobStatuses' }); return await callJobsSummaryAPI(spaceId, sourceId); }, onResolve: response => { - setJobStatus(currentJobStatus => ({ - ...currentJobStatus, - 'log-entry-rate': getJobStatus(getJobId(spaceId, sourceId, 'log-entry-rate'))(response), - })); + dispatch({ type: 'fetchedJobStatuses', payload: response, spaceId, sourceId }); }, onReject: err => { - setJobStatus(currentJobStatus => ({ - ...currentJobStatus, - 'log-entry-rate': 'unknown', - })); + dispatch({ type: 'failedFetchingJobStatuses' }); }, }, [indexPattern, spaceId, sourceId] @@ -106,82 +73,37 @@ export const useLogAnalysisJobs = ({ fetchJobStatus(); }, []); - const isSetupRequired = useMemo(() => { - return !Object.values(jobStatus).every(state => ['started', 'finished'].includes(state)); - }, [jobStatus]); - const isLoadingSetupStatus = useMemo(() => fetchJobStatusRequest.state === 'pending', [ fetchJobStatusRequest.state, ]); - const isSettingUpMlModule = useMemo(() => setupMlModuleRequest.state === 'pending', [ - setupMlModuleRequest.state, - ]); + const viewResults = useCallback(() => { + dispatch({ type: 'viewedResults' }); + }, []); - const didSetupFail = useMemo(() => { - const jobStates = Object.values(jobStatus); - return jobStates.filter(state => state === 'failed').length > 0; - }, [jobStatus]); + const retry = useCallback( + (start, end) => { + dispatch({ type: 'startedSetup' }); + cleanupMLResources() + .then(() => { + setupMlModule(start, end); + }) + .catch(() => { + dispatch({ type: 'failedSetup' }); + }); + }, + [cleanupMLResources, setupMlModule] + ); return { - jobStatus, - isSetupRequired, - isLoadingSetupStatus, - setupMlModule, setupMlModuleRequest, - isSettingUpMlModule, - didSetupFail, - hasCompletedSetup, + jobStatus: statusState.jobStatus, + isLoadingSetupStatus, + setup: setupMlModule, + retry, + setupStatus: statusState.setupStatus, + viewResults, }; }; export const LogAnalysisJobs = createContainer(useLogAnalysisJobs); - -const hasSuccessfullyCreatedJob = (jobId: string) => ( - jobSetupResponses: SetupMlModuleResponsePayload['jobs'] -) => - jobSetupResponses.filter( - jobSetupResponse => - jobSetupResponse.id === jobId && jobSetupResponse.success && !jobSetupResponse.error - ).length > 0; - -const hasSuccessfullyStartedDatafeed = (datafeedId: string) => ( - datafeedSetupResponses: SetupMlModuleResponsePayload['datafeeds'] -) => - datafeedSetupResponses.filter( - datafeedSetupResponse => - datafeedSetupResponse.id === datafeedId && - datafeedSetupResponse.success && - datafeedSetupResponse.started && - !datafeedSetupResponse.error - ).length > 0; - -const getJobStatus = (jobId: string) => (jobSummaries: FetchJobStatusResponsePayload): JobStatus => - jobSummaries - .filter(jobSummary => jobSummary.id === jobId) - .map( - (jobSummary): JobStatus => { - if (jobSummary.jobState === 'failed') { - return 'failed'; - } else if ( - jobSummary.jobState === 'closed' && - jobSummary.datafeedState === 'stopped' && - jobSummary.fullJob && - jobSummary.fullJob.finished_time != null - ) { - return 'finished'; - } else if ( - jobSummary.jobState === 'closed' || - jobSummary.jobState === 'closing' || - jobSummary.datafeedState === 'stopped' - ) { - return 'stopped'; - } else if (jobSummary.jobState === 'opening') { - return 'initializing'; - } else if (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') { - return 'started'; - } - - return 'unknown'; - } - )[0] || 'missing'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx new file mode 100644 index 0000000000000..91e70bf756559 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useState, useCallback } from 'react'; + +interface Props { + setupModule: (startTime?: number | undefined, endTime?: number | undefined) => void; + retrySetup: (startTime?: number | undefined, endTime?: number | undefined) => void; +} + +export const useAnalysisSetupState = ({ setupModule, retrySetup }: Props) => { + const [startTime, setStartTime] = useState(undefined); + const [endTime, setEndTime] = useState(undefined); + const setup = useCallback(() => { + return setupModule(startTime, endTime); + }, [setupModule, startTime, endTime]); + const retry = useCallback(() => { + return retrySetup(startTime, endTime); + }, [retrySetup, startTime, endTime]); + return { + setup, + retry, + setStartTime, + setEndTime, + startTime, + endTime, + }; +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx new file mode 100644 index 0000000000000..67554bb5d62be --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useReducer } from 'react'; +import { getDatafeedId, getJobId, JobType } from '../../../../common/log_analysis'; +import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api'; +import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; + +// combines and abstracts job and datafeed status +type JobStatus = + | 'unknown' + | 'missing' + | 'initializing' + | 'stopped' + | 'started' + | 'finished' + | 'failed'; + +export type SetupStatus = + | 'initializing' // acquiring job statuses to determine setup status + | 'unknown' // job status could not be acquired (failed request etc) + | 'required' // jobs are missing + | 'pending' // In the process of setting up the module for the first time or retrying, waiting for response + | 'succeeded' // setup succeeded, notifying user + | 'failed' // setup failed, notifying user + | 'hiddenAfterSuccess' // hide the setup screen and we show the results for the first time + | 'skipped'; // setup hidden because the module is in a correct state already + +interface StatusReducerState { + jobStatus: Record; + setupStatus: SetupStatus; +} + +type StatusReducerAction = + | { type: 'startedSetup' } + | { + type: 'finishedSetup'; + sourceId: string; + spaceId: string; + jobs: SetupMlModuleResponsePayload['jobs']; + datafeeds: SetupMlModuleResponsePayload['datafeeds']; + } + | { type: 'failedSetup' } + | { type: 'fetchingJobStatuses' } + | { + type: 'fetchedJobStatuses'; + spaceId: string; + sourceId: string; + payload: FetchJobStatusResponsePayload; + } + | { type: 'failedFetchingJobStatuses' } + | { type: 'viewedResults' }; + +const initialState: StatusReducerState = { + jobStatus: { + 'log-entry-rate': 'unknown', + }, + setupStatus: 'initializing', +}; + +function statusReducer(state: StatusReducerState, action: StatusReducerAction): StatusReducerState { + switch (action.type) { + case 'startedSetup': { + return { + jobStatus: { + 'log-entry-rate': 'initializing', + }, + setupStatus: 'pending', + }; + } + case 'finishedSetup': { + const { jobs, datafeeds, spaceId, sourceId } = action; + const nextJobStatus = { + ...state.jobStatus, + 'log-entry-rate': + hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, 'log-entry-rate'))(jobs) && + hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, 'log-entry-rate'))( + datafeeds + ) + ? ('started' as JobStatus) + : ('failed' as JobStatus), + }; + const nextSetupStatus = Object.values(nextJobStatus).every(jobState => + ['started'].includes(jobState) + ) + ? 'succeeded' + : 'failed'; + return { + jobStatus: nextJobStatus, + setupStatus: nextSetupStatus, + }; + } + case 'failedSetup': { + return { + jobStatus: { + ...state.jobStatus, + 'log-entry-rate': 'failed', + }, + setupStatus: 'failed', + }; + } + case 'fetchingJobStatuses': { + return { + ...state, + setupStatus: 'initializing', + }; + } + case 'fetchedJobStatuses': { + const { payload, spaceId, sourceId } = action; + const nextJobStatus = { + ...state.jobStatus, + 'log-entry-rate': getJobStatus(getJobId(spaceId, sourceId, 'log-entry-rate'))(payload), + }; + const nextSetupStatus = Object.values(nextJobStatus).every(jobState => + ['started', 'finished'].includes(jobState) + ) + ? 'skipped' + : 'required'; + return { + jobStatus: nextJobStatus, + setupStatus: nextSetupStatus, + }; + } + case 'failedFetchingJobStatuses': { + return { + ...state, + jobStatus: { + ...state.jobStatus, + 'log-entry-rate': 'unknown', + }, + }; + } + case 'viewedResults': { + return { + ...state, + setupStatus: 'hiddenAfterSuccess', + }; + } + default: { + return state; + } + } +} + +const hasSuccessfullyCreatedJob = (jobId: string) => ( + jobSetupResponses: SetupMlModuleResponsePayload['jobs'] +) => + jobSetupResponses.filter( + jobSetupResponse => + jobSetupResponse.id === jobId && jobSetupResponse.success && !jobSetupResponse.error + ).length > 0; + +const hasSuccessfullyStartedDatafeed = (datafeedId: string) => ( + datafeedSetupResponses: SetupMlModuleResponsePayload['datafeeds'] +) => + datafeedSetupResponses.filter( + datafeedSetupResponse => + datafeedSetupResponse.id === datafeedId && + datafeedSetupResponse.success && + datafeedSetupResponse.started && + !datafeedSetupResponse.error + ).length > 0; + +const getJobStatus = (jobId: string) => (jobSummaries: FetchJobStatusResponsePayload): JobStatus => + jobSummaries + .filter(jobSummary => jobSummary.id === jobId) + .map( + (jobSummary): JobStatus => { + if (jobSummary.jobState === 'failed') { + return 'failed'; + } else if ( + jobSummary.jobState === 'closed' && + jobSummary.datafeedState === 'stopped' && + jobSummary.fullJob && + jobSummary.fullJob.finished_time != null + ) { + return 'finished'; + } else if ( + jobSummary.jobState === 'closed' || + jobSummary.jobState === 'closing' || + jobSummary.datafeedState === 'stopped' + ) { + return 'stopped'; + } else if (jobSummary.jobState === 'opening') { + return 'initializing'; + } else if (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') { + return 'started'; + } + + return 'unknown'; + } + )[0] || 'missing'; + +export const useStatusState = () => { + return useReducer(statusReducer, initialState); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx index b19266f647d1e..b7a1c1a31159a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx @@ -5,15 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useContext, useEffect } from 'react'; -import chrome from 'ui/chrome'; +import React, { useContext } from 'react'; import { LoadingPage } from '../../../components/loading_page'; -import { - LogAnalysisCapabilities, - LogAnalysisJobs, - useLogAnalysisCleanup, -} from '../../../containers/logs/log_analysis'; +import { LogAnalysisCapabilities, LogAnalysisJobs } from '../../../containers/logs/log_analysis'; import { Source } from '../../../containers/source'; import { AnalysisResultsContent } from './page_results_content'; import { AnalysisSetupContent } from './page_setup_content'; @@ -23,27 +18,11 @@ export const AnalysisPageContent = () => { const { sourceId, source } = useContext(Source.Context); const { hasLogAnalysisCapabilites } = useContext(LogAnalysisCapabilities.Context); - const spaceId = chrome.getInjected('activeSpace').space.id; - - const { - isSetupRequired, - isLoadingSetupStatus, - setupMlModule, - isSettingUpMlModule, - didSetupFail, - hasCompletedSetup, - } = useContext(LogAnalysisJobs.Context); - - const { cleanupMLResources, isCleaningUp } = useLogAnalysisCleanup({ sourceId, spaceId }); - useEffect(() => { - if (didSetupFail) { - cleanupMLResources(); - } - }, [didSetupFail, cleanupMLResources]); + const { setup, retry, setupStatus, viewResults } = useContext(LogAnalysisJobs.Context); if (!hasLogAnalysisCapabilites) { return ; - } else if (isLoadingSetupStatus) { + } else if (setupStatus === 'initializing') { return ( { })} /> ); - } else if (isSetupRequired) { + } else if (setupStatus === 'skipped' || setupStatus === 'hiddenAfterSuccess') { + return ( + + ); + } else { return ( ); - } else { - return ; } }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx index 2e23f1b9dda02..120ae11b69f91 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React from 'react'; import { - EuiButtonEmpty, EuiPage, EuiPageBody, EuiPageContent, @@ -16,39 +15,31 @@ import { EuiText, EuiTitle, EuiSpacer, - EuiCallOut, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import euiStyled from '../../../../../../common/eui_styled_components'; import { useTrackPageview } from '../../../hooks/use_track_metric'; - -import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form'; -import { CreateMLJobsButton } from './create_ml_jobs_button'; +import { AnalysisSetupSteps } from './setup/steps'; +import { SetupStatus } from '../../../containers/logs/log_analysis'; interface AnalysisSetupContentProps { - setupMlModule: (startTime?: number | undefined, endTime?: number | undefined) => Promise; - isSettingUp: boolean; - didSetupFail: boolean; - isCleaningUpAFailedSetup: boolean; + setup: (startTime?: number | undefined, endTime?: number | undefined) => void; + retry: (startTime?: number | undefined, endTime?: number | undefined) => void; indexPattern: string; + viewResults: () => void; + setupStatus: SetupStatus; } -const errorTitle = i18n.translate('xpack.infra.analysisSetup.errorTitle', { - defaultMessage: 'Sorry, there was an error setting up Machine Learning', -}); - export const AnalysisSetupContent: React.FunctionComponent = ({ - setupMlModule, - isSettingUp, - didSetupFail, - isCleaningUpAFailedSetup, + setup, indexPattern, + viewResults, + retry, + setupStatus, }) => { useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' }); useTrackPageview({ app: 'infra_logs', path: 'analysis_setup', delay: 15000 }); - const [showTimeRangeForm, setShowTimeRangeForm] = useState(false); return ( @@ -76,52 +67,14 @@ export const AnalysisSetupContent: React.FunctionComponent - {showTimeRangeForm ? ( - <> - - - - ) : ( - <> - - - {' '} - setShowTimeRangeForm(true)}> - - - - - setupMlModule()} - /> - - )} - {didSetupFail && ( - <> - - - - - - - - )} + + @@ -137,15 +90,3 @@ const AnalysisPageContent = euiStyled(EuiPageContent)` const AnalysisSetupPage = euiStyled(EuiPage)` height: 100%; `; - -const ByDefaultText = euiStyled(EuiText).attrs({ size: 's' })` - & .euiButtonEmpty { - font-size: inherit; - line-height: inherit; - height: initial; - } - - & .euiButtonEmpty__content { - padding: 0; - } -`; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/analysis_setup_timerange_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/analysis_setup_timerange_form.tsx similarity index 75% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/analysis_setup_timerange_form.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/analysis_setup_timerange_form.tsx index 120670db83e8a..821971084edf2 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/analysis_setup_timerange_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/analysis_setup_timerange_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import moment, { Moment } from 'moment'; import { i18n } from '@kbn/i18n'; @@ -17,7 +17,6 @@ import { EuiFormControlLayout, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CreateMLJobsButton } from './create_ml_jobs_button'; const startTimeLabel = i18n.translate('xpack.infra.analysisSetup.startTimeLabel', { defaultMessage: 'Start time', @@ -46,18 +45,19 @@ function selectedDateToParam(selectedDate: Moment | null) { } export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ - isSettingUp: boolean; - setupMlModule: (startTime: number | undefined, endTime: number | undefined) => Promise; -}> = ({ isSettingUp, setupMlModule }) => { - const [startTime, setStartTime] = useState(null); - const [endTime, setEndTime] = useState(null); - + setStartTime: (startTime: number | undefined) => void; + setEndTime: (endTime: number | undefined) => void; + startTime: number | undefined; + endTime: number | undefined; +}> = ({ setStartTime, setEndTime, startTime, endTime }) => { const now = useMemo(() => moment(), []); - const selectedEndTimeIsToday = !endTime || endTime.isSame(now, 'day'); - - const onClickCreateJob = () => - setupMlModule(selectedDateToParam(startTime), selectedDateToParam(endTime)); - + const selectedEndTimeIsToday = !endTime || moment(endTime).isSame(now, 'day'); + const startTimeValue = useMemo(() => { + return startTime ? moment(startTime) : undefined; + }, [startTime]); + const endTimeValue = useMemo(() => { + return endTime ? moment(endTime) : undefined; + }, [endTime]); return ( setStartTime(null) } : undefined} + clear={startTime ? { onClick: () => setStartTime(undefined) } : undefined} > setStartTime(selectedDateToParam(date))} placeholder={startTimeDefaultDescription} maxDate={now} /> @@ -104,14 +104,16 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ label={endTimeLabel} > - setEndTime(null) } : undefined}> + setEndTime(undefined) } : undefined} + > setEndTime(selectedDateToParam(date))} placeholder={endTimeDefaultDescription} openToDate={now} - minDate={now} + minDate={startTimeValue} minTime={ selectedEndTimeIsToday ? now @@ -126,7 +128,6 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ - ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/create_ml_jobs_button.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/create_ml_jobs_button.tsx similarity index 75% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/create_ml_jobs_button.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/create_ml_jobs_button.tsx index 24caec70ed841..fd9fc20824785 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/create_ml_jobs_button.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/create_ml_jobs_button.tsx @@ -9,14 +9,13 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export const CreateMLJobsButton: React.FunctionComponent<{ - isLoading: boolean; onClick: () => void; -}> = ({ isLoading, onClick }) => { +}> = ({ onClick }) => { return ( - + ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/index.tsx new file mode 100644 index 0000000000000..1dde313845ea4 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/index.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSteps, EuiStepStatus } from '@elastic/eui'; +import { InitialConfiguration } from './initial_configuration'; +import { SetupProcess } from './setup_process'; +import { useAnalysisSetupState } from '../../../../../containers/logs/log_analysis/log_analysis_setup_state'; +import { SetupStatus } from '../../../../../containers/logs/log_analysis'; + +interface AnalysisSetupStepsProps { + setup: (startTime?: number | undefined, endTime?: number | undefined) => void; + retry: (startTime?: number | undefined, endTime?: number | undefined) => void; + viewResults: () => void; + indexPattern: string; + setupStatus: SetupStatus; +} + +export const AnalysisSetupSteps: React.FunctionComponent = ({ + setup: setupModule, + retry: retrySetup, + viewResults, + indexPattern, + setupStatus, +}: AnalysisSetupStepsProps) => { + const { setup, retry, setStartTime, setEndTime, startTime, endTime } = useAnalysisSetupState({ + setupModule, + retrySetup, + }); + + const steps = [ + { + title: i18n.translate('xpack.infra.analysisSetup.stepOneTitle', { + defaultMessage: 'Configuration (optional)', + }), + children: ( + + ), + }, + { + title: i18n.translate('xpack.infra.analysisSetup.stepTwoTitle', { + defaultMessage: 'Create ML jobs', + }), + children: ( + + ), + status: + setupStatus === 'pending' + ? ('incomplete' as EuiStepStatus) + : setupStatus === 'failed' + ? ('danger' as EuiStepStatus) + : setupStatus === 'succeeded' + ? ('complete' as EuiStepStatus) + : undefined, + }, + ]; + + return ; +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/initial_configuration.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/initial_configuration.tsx new file mode 100644 index 0000000000000..056aa4d05044c --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/initial_configuration.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AnalysisSetupTimerangeForm } from '../analysis_setup_timerange_form'; + +interface InitialConfigurationProps { + setStartTime: (startTime: number | undefined) => void; + setEndTime: (endTime: number | undefined) => void; + startTime: number | undefined; + endTime: number | undefined; +} + +export const InitialConfiguration: React.FunctionComponent = ({ + setStartTime, + setEndTime, + startTime, + endTime, +}: InitialConfigurationProps) => { + const [showTimeRangeForm, setShowTimeRangeForm] = useState(false); + return ( + <> + {showTimeRangeForm ? ( + <> + + + + ) : ( + <> + + + {' '} + { + e.preventDefault(); + setShowTimeRangeForm(true); + }} + > + + + + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/setup_process.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/setup_process.tsx new file mode 100644 index 0000000000000..d08d52fd159ae --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/setup_process.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiLoadingSpinner, + EuiButton, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CreateMLJobsButton } from '../create_ml_jobs_button'; +import { SetupStatus } from '../../../../../containers/logs/log_analysis'; + +interface Props { + viewResults: () => void; + setup: () => void; + retry: () => void; + indexPattern: string; + setupStatus: SetupStatus; +} + +export const SetupProcess: React.FunctionComponent = ({ + viewResults, + setup, + retry, + indexPattern, + setupStatus, +}: Props) => { + return ( + + {setupStatus === 'pending' ? ( + + + + + + + + + ) : setupStatus === 'failed' ? ( + <> + + + + + + + ) : setupStatus === 'succeeded' ? ( + <> + + + + + + + ) : ( + + )} + + ); +};