From 603e27edb185469f7730cf3861db1d6a77264042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 22 Oct 2019 01:20:22 +0200 Subject: [PATCH] [Logs UI] Provide index name pattern choice during ML job setup (#48231) This fixes #48219 by adding the option for the user to select a subset of the configured log indices during the setup process. It also surfaces the errors returned by Kibana when the setup fails. --- .../infra/common/log_analysis/log_analysis.ts | 5 + .../plugins/infra/common/utility_types.ts | 5 + .../log_analysis/api/ml_setup_module_api.ts | 42 ++++-- .../logs/log_analysis/log_analysis_jobs.tsx | 32 ++--- .../log_analysis/log_analysis_setup_state.tsx | 62 ++++++-- .../log_analysis_status_state.tsx | 25 +++- .../pages/logs/analysis/page_content.tsx | 19 ++- .../logs/analysis/page_setup_content.tsx | 23 +-- .../setup/analysis_setup_timerange_form.tsx | 134 ------------------ .../public/pages/logs/analysis/setup/index.ts | 7 + .../analysis_setup_indices_form.tsx | 88 ++++++++++++ .../analysis_setup_timerange_form.tsx | 131 +++++++++++++++++ .../setup/initial_configuration_step/index.ts | 7 + .../initial_configuration_step.tsx | 60 ++++++++ .../create_ml_jobs_button.tsx | 7 +- .../logs/analysis/setup/process_step/index.ts | 7 + .../process_step.tsx} | 50 ++++--- .../recreate_ml_jobs_button.tsx | 7 +- .../{steps/index.tsx => setup_steps.tsx} | 44 ++++-- .../setup/steps/initial_configuration.tsx | 62 -------- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 22 files changed, 527 insertions(+), 294 deletions(-) delete mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/analysis_setup_timerange_form.tsx create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/index.ts create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_timerange_form.tsx create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/index.ts create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx rename x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/{ => process_step}/create_ml_jobs_button.tsx (83%) create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/index.ts rename x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/{steps/setup_process.tsx => process_step/process_step.tsx} (68%) rename x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/{ => process_step}/recreate_ml_jobs_button.tsx (86%) rename x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/{steps/index.tsx => setup_steps.tsx} (60%) delete mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/initial_configuration.tsx diff --git a/x-pack/legacy/plugins/infra/common/log_analysis/log_analysis.ts b/x-pack/legacy/plugins/infra/common/log_analysis/log_analysis.ts index 89e42f69c9daf..4a6f20d549799 100644 --- a/x-pack/legacy/plugins/infra/common/log_analysis/log_analysis.ts +++ b/x-pack/legacy/plugins/infra/common/log_analysis/log_analysis.ts @@ -54,3 +54,8 @@ export const isSetupStatusWithResults = (setupStatus: SetupStatus) => ['skipped', 'hiddenAfterSuccess', 'skippedButReconfigurable', 'skippedButUpdatable'].includes( setupStatus ); + +const KIBANA_SAMPLE_DATA_INDICES = ['kibana_sample_data_logs*']; + +export const isExampleDataIndex = (indexName: string) => + KIBANA_SAMPLE_DATA_INDICES.includes(indexName); diff --git a/x-pack/legacy/plugins/infra/common/utility_types.ts b/x-pack/legacy/plugins/infra/common/utility_types.ts index e2c1a5aaf885b..1d1148f8716a1 100644 --- a/x-pack/legacy/plugins/infra/common/utility_types.ts +++ b/x-pack/legacy/plugins/infra/common/utility_types.ts @@ -11,6 +11,11 @@ export type Pick3 = T & + { + [prop in Prop]-?: NonNullable; + }; + /** * Portions of below code are derived from https://github.com/tycho01/typical * under the MIT License diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts index 92078b23085c3..1c937513c7950 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts @@ -92,22 +92,34 @@ const setupMlModuleRequestPayloadRT = rt.intersection([ setupMlModuleRequestParamsRT, ]); +const setupErrorResponseRT = rt.type({ + msg: rt.string, +}); + +const datafeedSetupResponseRT = rt.intersection([ + rt.type({ + id: rt.string, + started: rt.boolean, + success: rt.boolean, + }), + rt.partial({ + error: setupErrorResponseRT, + }), +]); + +const jobSetupResponseRT = rt.intersection([ + rt.type({ + id: rt.string, + success: rt.boolean, + }), + rt.partial({ + error: setupErrorResponseRT, + }), +]); + const setupMlModuleResponsePayloadRT = rt.type({ - datafeeds: rt.array( - rt.type({ - id: rt.string, - started: rt.boolean, - success: rt.boolean, - error: rt.any, - }) - ), - jobs: rt.array( - rt.type({ - id: rt.string, - success: rt.boolean, - error: rt.any, - }) - ), + datafeeds: rt.array(datafeedSetupResponseRT), + jobs: rt.array(jobSetupResponseRT), }); export type SetupMlModuleResponsePayload = rt.TypeOf; 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 278c6fd5ee9f9..163f0e39d1228 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 @@ -16,7 +16,6 @@ import { useLogAnalysisCleanup } from './log_analysis_cleanup'; import { useStatusState } from './log_analysis_status_state'; const MODULE_ID = 'logs_ui_analysis'; -const SAMPLE_DATA_INDEX = 'kibana_sample_data_logs*'; export const useLogAnalysisJobs = ({ indexPattern, @@ -29,11 +28,10 @@ export const useLogAnalysisJobs = ({ spaceId: string; timeField: string; }) => { - const filteredIndexPattern = useMemo(() => removeSampleDataIndex(indexPattern), [indexPattern]); const { cleanupMLResources } = useLogAnalysisCleanup({ sourceId, spaceId }); const [statusState, dispatch] = useStatusState({ bucketSpan, - indexPattern: filteredIndexPattern, + indexPattern, timestampField: timeField, }); @@ -62,7 +60,11 @@ export const useLogAnalysisJobs = ({ const [setupMlModuleRequest, setupMlModule] = useTrackedPromise( { cancelPreviousOn: 'resolution', - createPromise: async (start, end) => { + createPromise: async ( + indices: string[], + start: number | undefined, + end: number | undefined + ) => { dispatch({ type: 'startedSetup' }); return await callSetupMlModuleAPI( MODULE_ID, @@ -70,7 +72,7 @@ export const useLogAnalysisJobs = ({ end, spaceId, sourceId, - filteredIndexPattern, + indices.join(','), timeField, bucketSpan ); @@ -82,7 +84,7 @@ export const useLogAnalysisJobs = ({ dispatch({ type: 'failedSetup' }); }, }, - [filteredIndexPattern, spaceId, sourceId, timeField, bucketSpan] + [spaceId, sourceId, timeField, bucketSpan] ); const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise( @@ -99,7 +101,7 @@ export const useLogAnalysisJobs = ({ dispatch({ type: 'failedFetchingJobStatuses' }); }, }, - [filteredIndexPattern, spaceId, sourceId] + [spaceId, sourceId] ); const isLoadingSetupStatus = useMemo( @@ -108,16 +110,18 @@ export const useLogAnalysisJobs = ({ [fetchJobStatusRequest.state, fetchModuleDefinitionRequest.state] ); + const availableIndices = useMemo(() => indexPattern.split(','), [indexPattern]); + const viewResults = useCallback(() => { dispatch({ type: 'viewedResults' }); }, []); const cleanupAndSetup = useCallback( - (start, end) => { + (indices: string[], start: number | undefined, end: number | undefined) => { dispatch({ type: 'startedSetup' }); cleanupMLResources() .then(() => { - setupMlModule(start, end); + setupMlModule(indices, start, end); }) .catch(() => { dispatch({ type: 'failedSetup' }); @@ -145,9 +149,11 @@ export const useLogAnalysisJobs = ({ }, [sourceId, spaceId]); return { + availableIndices, fetchJobStatus, isLoadingSetupStatus, jobStatus: statusState.jobStatus, + lastSetupErrorMessages: statusState.lastSetupErrorMessages, cleanupAndSetup, setup: setupMlModule, setupMlModuleRequest, @@ -160,11 +166,3 @@ export const useLogAnalysisJobs = ({ }; export const LogAnalysisJobs = createContainer(useLogAnalysisJobs); -// -// This is needed due to: https://github.com/elastic/kibana/issues/43671 -const removeSampleDataIndex = (indexPattern: string) => { - return indexPattern - .split(',') - .filter(index => index !== SAMPLE_DATA_INDEX) - .join(','); -}; 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 index 9f108b0c50f53..7942657018455 100644 --- 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 @@ -3,35 +3,81 @@ * 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'; -type SetupHandler = (startTime?: number | undefined, endTime?: number | undefined) => void; +import { useState, useCallback, useMemo } from 'react'; -interface Props { +import { isExampleDataIndex } from '../../../../common/log_analysis'; + +type SetupHandler = ( + indices: string[], + startTime: number | undefined, + endTime: number | undefined +) => void; + +interface AnalysisSetupStateArguments { + availableIndices: string[]; cleanupAndSetupModule: SetupHandler; setupModule: SetupHandler; } +type IndicesSelection = Record; + +type ValidationErrors = 'TOO_FEW_SELECTED_INDICES'; + const fourWeeksInMs = 86400000 * 7 * 4; -export const useAnalysisSetupState = ({ setupModule, cleanupAndSetupModule }: Props) => { +export const useAnalysisSetupState = ({ + availableIndices, + cleanupAndSetupModule, + setupModule, +}: AnalysisSetupStateArguments) => { const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); + const [selectedIndices, setSelectedIndices] = useState( + availableIndices.reduce( + (indexMap, indexName) => ({ + ...indexMap, + [indexName]: !(availableIndices.length > 1 && isExampleDataIndex(indexName)), + }), + {} + ) + ); + + const selectedIndexNames = useMemo( + () => + Object.entries(selectedIndices) + .filter(([_indexName, isSelected]) => isSelected) + .map(([indexName]) => indexName), + [selectedIndices] + ); + const setup = useCallback(() => { - return setupModule(startTime, endTime); - }, [setupModule, startTime, endTime]); + return setupModule(selectedIndexNames, startTime, endTime); + }, [setupModule, selectedIndexNames, startTime, endTime]); const cleanupAndSetup = useCallback(() => { - return cleanupAndSetupModule(startTime, endTime); - }, [cleanupAndSetupModule, startTime, endTime]); + return cleanupAndSetupModule(selectedIndexNames, startTime, endTime); + }, [cleanupAndSetupModule, selectedIndexNames, startTime, endTime]); + + const validationErrors: ValidationErrors[] = useMemo( + () => + Object.values(selectedIndices).some(isSelected => isSelected) + ? [] + : ['TOO_FEW_SELECTED_INDICES' as const], + [selectedIndices] + ); return { cleanupAndSetup, endTime, + selectedIndexNames, + selectedIndices, setEndTime, + setSelectedIndices, setStartTime, setup, startTime, + validationErrors, }; }; 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 index 85c2ff93051ad..927ce4c3e1f58 100644 --- 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 @@ -18,11 +18,13 @@ import { import { FetchJobStatusResponsePayload, JobSummary } from './api/ml_get_jobs_summary_api'; import { GetMlModuleResponsePayload, JobDefinition } from './api/ml_get_module'; import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; +import { MandatoryProperty } from '../../../../common/utility_types'; interface StatusReducerState { jobDefinitions: JobDefinition[]; jobStatus: Record; jobSummaries: JobSummary[]; + lastSetupErrorMessages: string[]; setupStatus: SetupStatus; sourceConfiguration: JobSourceConfiguration; } @@ -69,6 +71,7 @@ const createInitialState = (sourceConfiguration: JobSourceConfiguration): Status 'log-entry-rate': 'unknown', }, jobSummaries: [], + lastSetupErrorMessages: [], setupStatus: 'initializing', sourceConfiguration, }); @@ -101,9 +104,18 @@ function statusReducer(state: StatusReducerState, action: StatusReducerAction): ) ? 'succeeded' : 'failed'; + const nextErrorMessages = [ + ...Object.values(datafeeds) + .filter(hasError) + .map(datafeed => datafeed.error.msg), + ...Object.values(jobs) + .filter(hasError) + .map(job => job.error.msg), + ]; return { ...state, jobStatus: nextJobStatus, + lastSetupErrorMessages: nextErrorMessages, setupStatus: nextSetupStatus, }; } @@ -348,11 +360,22 @@ const isJobConfigurationConsistent = ( return ( jobConfiguration && jobConfiguration.bucketSpan === sourceConfiguration.bucketSpan && - jobConfiguration.indexPattern === sourceConfiguration.indexPattern && + jobConfiguration.indexPattern && + isIndexPatternSubset(jobConfiguration.indexPattern, sourceConfiguration.indexPattern) && jobConfiguration.timestampField === sourceConfiguration.timestampField ); }); +const isIndexPatternSubset = (indexPatternSubset: string, indexPatternSuperset: string) => { + const subsetSubPatterns = indexPatternSubset.split(','); + const supersetSubPatterns = new Set(indexPatternSuperset.split(',')); + + return subsetSubPatterns.every(subPattern => supersetSubPatterns.has(subPattern)); +}; + +const hasError = (value: Value): value is MandatoryProperty => + value.error != null; + export const useStatusState = (sourceConfiguration: JobSourceConfiguration) => { return useReducer(statusReducer, sourceConfiguration, createInitialState); }; 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 2b83007e2ab99..04d7520c0ca88 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 @@ -17,12 +17,18 @@ import { AnalysisUnavailableContent } from './page_unavailable_content'; import { AnalysisSetupStatusUnknownContent } from './page_setup_status_unknown'; export const AnalysisPageContent = () => { - const { sourceId, source } = useContext(Source.Context); + const { sourceId } = useContext(Source.Context); const { hasLogAnalysisCapabilites } = useContext(LogAnalysisCapabilities.Context); - const { setup, cleanupAndSetup, setupStatus, viewResults, fetchJobStatus } = useContext( - LogAnalysisJobs.Context - ); + const { + availableIndices, + cleanupAndSetup, + fetchJobStatus, + lastSetupErrorMessages, + setup, + setupStatus, + viewResults, + } = useContext(LogAnalysisJobs.Context); useEffect(() => { fetchJobStatus(); @@ -50,10 +56,11 @@ export const AnalysisPageContent = () => { } 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 16da61bd1d9c1..097cccf5dca33 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 @@ -20,21 +20,27 @@ import { FormattedMessage } from '@kbn/i18n/react'; import euiStyled from '../../../../../../common/eui_styled_components'; import { SetupStatus } from '../../../../common/log_analysis'; import { useTrackPageview } from '../../../hooks/use_track_metric'; -import { AnalysisSetupSteps } from './setup/steps'; +import { AnalysisSetupSteps } from './setup'; -type SetupHandler = (startTime?: number | undefined, endTime?: number | undefined) => void; +type SetupHandler = ( + indices: string[], + startTime: number | undefined, + endTime: number | undefined +) => void; interface AnalysisSetupContentProps { + availableIndices: string[]; cleanupAndSetup: SetupHandler; - indexPattern: string; + errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; viewResults: () => void; } export const AnalysisSetupContent: React.FunctionComponent = ({ + availableIndices, cleanupAndSetup, - indexPattern, + errorMessages, setup, setupStatus, viewResults, @@ -71,11 +77,12 @@ export const AnalysisSetupContent: React.FunctionComponent @@ -86,7 +93,7 @@ export const AnalysisSetupContent: React.FunctionComponent void; - setEndTime: (endTime: number | undefined) => void; - startTime: number | undefined; - endTime: number | undefined; -}> = ({ setStartTime, setEndTime, startTime, endTime }) => { - const now = useMemo(() => moment(), []); - 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 ( - - - } - description={ - - } - > - - - setStartTime(undefined) } : undefined} - > - setStartTime(selectedDateToParam(date))} - placeholder={startTimeDefaultDescription} - maxDate={now} - /> - - - - - - setEndTime(undefined) } : undefined} - > - setEndTime(selectedDateToParam(date))} - placeholder={endTimeDefaultDescription} - openToDate={now} - minDate={startTimeValue} - minTime={ - selectedEndTimeIsToday - ? now - : moment() - .hour(0) - .minutes(0) - } - maxTime={moment() - .hour(23) - .minutes(59)} - /> - - - - - - ); -}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/index.ts new file mode 100644 index 0000000000000..eba07aaa17fd2 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './setup_steps'; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx new file mode 100644 index 0000000000000..defcefd69a7ab --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -0,0 +1,88 @@ +/* + * 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 { EuiCheckboxGroup, EuiCode, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; + +export type IndicesSelection = Record; + +export type IndicesValidationError = 'TOO_FEW_SELECTED_INDICES'; + +export const AnalysisSetupIndicesForm: React.FunctionComponent<{ + indices: IndicesSelection; + onChangeSelectedIndices: (selectedIndices: IndicesSelection) => void; + validationErrors?: IndicesValidationError[]; +}> = ({ indices, onChangeSelectedIndices, validationErrors = [] }) => { + const choices = useMemo( + () => + Object.keys(indices).map(indexName => ({ + id: indexName, + label: {indexName}, + })), + [indices] + ); + + const handleCheckboxGroupChange = useCallback( + indexName => { + onChangeSelectedIndices({ + ...indices, + [indexName]: !indices[indexName], + }); + }, + [indices, onChangeSelectedIndices] + ); + + return ( + + } + description={ + + } + > + 0} + label={indicesSelectionLabel} + labelType="legend" + > + + + + ); +}; + +const indicesSelectionLabel = i18n.translate('xpack.infra.analysisSetup.indicesSelectionLabel', { + defaultMessage: 'Indices', +}); + +const formatValidationError = (validationError: IndicesValidationError) => { + switch (validationError) { + case 'TOO_FEW_SELECTED_INDICES': + return i18n.translate( + 'xpack.infra.analysisSetup.indicesSelectionTooFewSelectedIndicesDescription', + { + defaultMessage: 'Select at least one index name.', + } + ); + } +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_timerange_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_timerange_form.tsx new file mode 100644 index 0000000000000..e966336567e59 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_timerange_form.tsx @@ -0,0 +1,131 @@ +/* + * 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, { useMemo } from 'react'; +import moment, { Moment } from 'moment'; + +import { i18n } from '@kbn/i18n'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiDatePicker, + EuiFlexGroup, + EuiFormControlLayout, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +const startTimeLabel = i18n.translate('xpack.infra.analysisSetup.startTimeLabel', { + defaultMessage: 'Start time', +}); +const endTimeLabel = i18n.translate('xpack.infra.analysisSetup.endTimeLabel', { + defaultMessage: 'End time', +}); +const startTimeDefaultDescription = i18n.translate( + 'xpack.infra.analysisSetup.startTimeDefaultDescription', + { + defaultMessage: 'Start of log indices', + } +); +const endTimeDefaultDescription = i18n.translate( + 'xpack.infra.analysisSetup.endTimeDefaultDescription', + { + defaultMessage: 'Indefinitely', + } +); + +function selectedDateToParam(selectedDate: Moment | null) { + if (selectedDate) { + return selectedDate.valueOf(); // To ms unix timestamp + } + return undefined; +} + +export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ + 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 || moment(endTime).isSame(now, 'day'); + const startTimeValue = useMemo(() => { + return startTime ? moment(startTime) : undefined; + }, [startTime]); + const endTimeValue = useMemo(() => { + return endTime ? moment(endTime) : undefined; + }, [endTime]); + return ( + + } + description={ + + } + > + + + setStartTime(undefined) } : undefined} + > + setStartTime(selectedDateToParam(date))} + placeholder={startTimeDefaultDescription} + maxDate={now} + /> + + + + + + setEndTime(undefined) } : undefined} + > + setEndTime(selectedDateToParam(date))} + placeholder={endTimeDefaultDescription} + openToDate={now} + minDate={startTimeValue} + minTime={ + selectedEndTimeIsToday + ? now + : moment() + .hour(0) + .minutes(0) + } + maxTime={moment() + .hour(23) + .minutes(59)} + /> + + + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/index.ts new file mode 100644 index 0000000000000..c44079f0e19df --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './initial_configuration_step'; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx new file mode 100644 index 0000000000000..929fba26f2323 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiSpacer, EuiForm } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { + AnalysisSetupIndicesForm, + IndicesSelection, + IndicesValidationError, +} from './analysis_setup_indices_form'; +import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form'; + +interface InitialConfigurationStepProps { + setStartTime: (startTime: number | undefined) => void; + setEndTime: (endTime: number | undefined) => void; + startTime: number | undefined; + endTime: number | undefined; + selectedIndices: IndicesSelection; + setSelectedIndices: (selectedIndices: IndicesSelection) => void; + validationErrors?: IndicesValidationError[]; +} + +export const InitialConfigurationStep: React.FunctionComponent = ({ + setStartTime, + setEndTime, + startTime, + endTime, + selectedIndices, + setSelectedIndices, + validationErrors = [], +}: InitialConfigurationStepProps) => { + const indicesFormValidationErrors = useMemo( + () => + validationErrors.filter(validationError => validationError === 'TOO_FEW_SELECTED_INDICES'), + [validationErrors] + ); + + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/create_ml_jobs_button.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/create_ml_jobs_button.tsx similarity index 83% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/create_ml_jobs_button.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/create_ml_jobs_button.tsx index a32d12509ceec..ae91808152432 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/create_ml_jobs_button.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/create_ml_jobs_button.tsx @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; export const CreateMLJobsButton: React.FunctionComponent<{ + isDisabled?: boolean; onClick: () => void; -}> = ({ onClick }) => { +}> = ({ isDisabled, onClick }) => { return ( - + void; - setup: () => void; +interface ProcessStepProps { cleanupAndSetup: () => void; - indexPattern: string; + errorMessages: string[]; + isConfigurationValid: boolean; + setup: () => void; setupStatus: SetupStatus; + viewResults: () => void; } -export const SetupProcess: React.FunctionComponent = ({ - viewResults, - setup, +export const ProcessStep: React.FunctionComponent = ({ cleanupAndSetup, - indexPattern, + errorMessages, + isConfigurationValid, + setup, setupStatus, -}: Props) => { + viewResults, +}) => { return ( {setupStatus === 'pending' ? ( @@ -52,13 +57,15 @@ export const SetupProcess: React.FunctionComponent = ({ <> + {errorMessages.map(errorMessage => ( + + {errorMessage} + + ))} + = ({ ) : setupStatus === 'requiredForUpdate' || setupStatus === 'requiredForReconfiguration' ? ( - + ) : ( - + )} ); }; + +const errorCalloutTitle = i18n.translate( + 'xpack.infra.analysisSetup.steps.setupProcess.errorCalloutTitle', + { + defaultMessage: 'An error occurred', + } +); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/recreate_ml_jobs_button.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/recreate_ml_jobs_button.tsx similarity index 86% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/recreate_ml_jobs_button.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/recreate_ml_jobs_button.tsx index 0232c1167f194..61ab7d1248efb 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/recreate_ml_jobs_button.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/recreate_ml_jobs_button.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; export const RecreateMLJobsButton: React.FunctionComponent<{ + isDisabled?: boolean; onClick: () => void; -}> = ({ onClick }) => { +}> = ({ isDisabled, onClick }) => { return ( <> - + void; +type SetupHandler = ( + indices: string[], + startTime: number | undefined, + endTime: number | undefined +) => void; interface AnalysisSetupStepsProps { + availableIndices: string[]; cleanupAndSetup: SetupHandler; - indexPattern: string; + errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; viewResults: () => void; } export const AnalysisSetupSteps: React.FunctionComponent = ({ + availableIndices, cleanupAndSetup: cleanupAndSetupModule, - indexPattern, + errorMessages, setup: setupModule, setupStatus, viewResults, @@ -37,36 +43,44 @@ export const AnalysisSetupSteps: React.FunctionComponent ), }, { - title: i18n.translate('xpack.infra.analysisSetup.stepTwoTitle', { + title: i18n.translate('xpack.infra.analysisSetup.actionStepTitle', { defaultMessage: 'Create ML job', }), children: ( - ), status: 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 deleted file mode 100644 index b687924ff57aa..0000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/steps/initial_configuration.tsx +++ /dev/null @@ -1,62 +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; - * 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/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ae118b0329b59..d3d66cd104120 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5188,13 +5188,11 @@ "xpack.infra.waffleTime.stopRefreshingButtonLabel": "更新中止", "xpack.infra.analysisSetup.analysisSetupDescription": "機械学習を使用して自動的に異常ログレートカウントを検出します。", "xpack.infra.analysisSetup.analysisSetupTitle": "機械学習分析を有効にする", - "xpack.infra.analysisSetup.configureTimeRange": "時間範囲を構成しますか?", "xpack.infra.analysisSetup.createMlJobButton": "ML ジョブを作成", "xpack.infra.analysisSetup.endTimeDefaultDescription": "永久", "xpack.infra.analysisSetup.endTimeLabel": "終了時刻", "xpack.infra.analysisSetup.startTimeDefaultDescription": "ログインデックスの開始地点です。", "xpack.infra.analysisSetup.startTimeLabel": "開始時刻", - "xpack.infra.analysisSetup.timeRangeByDefault": "デフォルトで、ログインデックス内のすべての過去と未来のログメッセージを分析します。", "xpack.infra.analysisSetup.timeRangeDescription": "デフォルトで、機械学習はログインデックスの始めからログメッセージを分析し、永久に継続します。別の開始日、終了日、または両方を指定できます。", "xpack.infra.analysisSetup.timeRangeTitle": "時間範囲の選択", "xpack.infra.chartSection.missingMetricDataBody": "このチャートはデータが欠けています。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 451770a9eed82..c7448d0a7972c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5191,13 +5191,11 @@ "xpack.infra.waffleTime.stopRefreshingButtonLabel": "停止刷新", "xpack.infra.analysisSetup.analysisSetupDescription": "使用 Machine Learning 自动检测异常日志速率计数。", "xpack.infra.analysisSetup.analysisSetupTitle": "启用 Machine Learning 分析", - "xpack.infra.analysisSetup.configureTimeRange": "配置时间范围?", "xpack.infra.analysisSetup.createMlJobButton": "创建 ML 作业", "xpack.infra.analysisSetup.endTimeDefaultDescription": "无限期", "xpack.infra.analysisSetup.endTimeLabel": "结束时间", "xpack.infra.analysisSetup.startTimeDefaultDescription": "日志索引的开始时间", "xpack.infra.analysisSetup.startTimeLabel": "开始时间", - "xpack.infra.analysisSetup.timeRangeByDefault": "默认情况下,我们将分析日志索引中的所有过去和未来日志消息。", "xpack.infra.analysisSetup.timeRangeDescription": "默认情况下,Machine Learning 分析自日志索引开始时间起的日志消息并无限期继续下去。您可以指定不同的开始日期或/和结束日期。", "xpack.infra.analysisSetup.timeRangeTitle": "选择时间范围", "xpack.infra.chartSection.missingMetricDataBody": "此图表的数据缺失。",