diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx index 06dbf5315b83a..e089ae912e112 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -10,15 +10,21 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; import { LoadingOverlayWrapper } from '../../../loading_overlay_wrapper'; import { IndexSetupRow } from './index_setup_row'; -import { AvailableIndex } from './validation'; +import { AvailableIndex, ValidationIndicesError } from './validation'; export const AnalysisSetupIndicesForm: React.FunctionComponent<{ disabled?: boolean; indices: AvailableIndex[]; isValidating: boolean; onChangeSelectedIndices: (selectedIndices: AvailableIndex[]) => void; - valid: boolean; -}> = ({ disabled = false, indices, isValidating, onChangeSelectedIndices, valid }) => { + validationErrors?: ValidationIndicesError[]; +}> = ({ + disabled = false, + indices, + isValidating, + onChangeSelectedIndices, + validationErrors = [], +}) => { const changeIsIndexSelected = useCallback( (indexName: string, isSelected: boolean) => { onChangeSelectedIndices( @@ -41,6 +47,8 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ [indices, onChangeSelectedIndices] ); + const isInvalid = validationErrors.length > 0; + return ( - + <> {indices.map(index => ( void; startTime: number | undefined; endTime: number | undefined; -}> = ({ disabled = false, setStartTime, setEndTime, startTime, endTime }) => { - const now = useMemo(() => moment(), []); + validationErrors?: TimeRangeValidationError[]; +}> = ({ + disabled = false, + setStartTime, + setEndTime, + startTime, + endTime, + validationErrors = [], +}) => { + const [now] = useState(() => 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]); + + const startTimeValidationErrorMessages = useMemo( + () => getStartTimeValidationErrorMessages(validationErrors), + [validationErrors] + ); + + const endTimeValidationErrorMessages = useMemo( + () => getEndTimeValidationErrorMessages(validationErrors), + [validationErrors] + ); + return ( } > - + 0} + label={startTimeLabel} + > setStartTime(undefined) } : undefined} @@ -91,7 +117,12 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ - + 0} + label={endTimeLabel} + > setEndTime(undefined) } : undefined} @@ -122,3 +153,31 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ ); }; + +const getStartTimeValidationErrorMessages = (validationErrors: TimeRangeValidationError[]) => + validationErrors.flatMap(validationError => { + switch (validationError.error) { + case 'INVALID_TIME_RANGE': + return [ + i18n.translate('xpack.infra.analysisSetup.startTimeBeforeEndTimeErrorMessage', { + defaultMessage: 'The start time must be before the end time.', + }), + ]; + default: + return []; + } + }); + +const getEndTimeValidationErrorMessages = (validationErrors: TimeRangeValidationError[]) => + validationErrors.flatMap(validationError => { + switch (validationError.error) { + case 'INVALID_TIME_RANGE': + return [ + i18n.translate('xpack.infra.analysisSetup.endTimeAfterStartTimeErrorMessage', { + defaultMessage: 'The end time must be after the start time.', + }), + ]; + default: + return []; + } + }); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx index 18dc2e5aa9bd1..2eb67e0c0ce76 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; import { DatasetFilter } from '../../../../../common/log_analysis'; import { IndexSetupDatasetFilter } from './index_setup_dataset_filter'; -import { AvailableIndex, ValidationIndicesUIError } from './validation'; +import { AvailableIndex, ValidationUIError } from './validation'; export const IndexSetupRow: React.FC<{ index: AvailableIndex; @@ -61,7 +61,7 @@ export const IndexSetupRow: React.FC<{ ); }; -const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => { +const formatValidationError = (errors: ValidationUIError[]): React.ReactNode => { return errors.map(error => { switch (error.error) { case 'INDEX_NOT_FOUND': diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx index 85aa7ce513248..c9b14a1ffe47a 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx @@ -4,16 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiForm, EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiForm, EuiSpacer } from '@elastic/eui'; import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; - import { SetupStatus } from '../../../../../common/log_analysis'; import { AnalysisSetupIndicesForm } from './analysis_setup_indices_form'; import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form'; -import { AvailableIndex, ValidationIndicesUIError } from './validation'; +import { + AvailableIndex, + TimeRangeValidationError, + timeRangeValidationErrorRT, + ValidationIndicesError, + validationIndicesErrorRT, + ValidationUIError, +} from './validation'; interface InitialConfigurationStepProps { setStartTime: (startTime: number | undefined) => void; @@ -24,7 +30,7 @@ interface InitialConfigurationStepProps { validatedIndices: AvailableIndex[]; setupStatus: SetupStatus; setValidatedIndices: (selectedIndices: AvailableIndex[]) => void; - validationErrors?: ValidationIndicesUIError[]; + validationErrors?: ValidationUIError[]; } export const createInitialConfigurationStep = ( @@ -47,6 +53,11 @@ export const InitialConfigurationStep: React.FunctionComponent { const disabled = useMemo(() => !editableFormStatus.includes(setupStatus.type), [setupStatus]); + const [indexValidationErrors, timeRangeValidationErrors, globalValidationErrors] = useMemo( + () => partitionValidationErrors(validationErrors), + [validationErrors] + ); + return ( <> @@ -57,16 +68,17 @@ export const InitialConfigurationStep: React.FunctionComponent - + ); @@ -88,7 +100,7 @@ const initialConfigurationStepTitle = i18n.translate( } ); -const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ errors }) => { +const ValidationErrors: React.FC<{ errors: ValidationUIError[] }> = ({ errors }) => { if (errors.length === 0) { return null; } @@ -107,7 +119,7 @@ const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ er ); }; -const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode => { +const formatValidationError = (error: ValidationUIError): React.ReactNode => { switch (error.error) { case 'NETWORK_ERROR': return ( @@ -129,3 +141,19 @@ const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode return ''; } }; + +const partitionValidationErrors = (validationErrors: ValidationUIError[]) => + validationErrors.reduce< + [ValidationIndicesError[], TimeRangeValidationError[], ValidationUIError[]] + >( + ([indicesErrors, timeRangeErrors, otherErrors], error) => { + if (validationIndicesErrorRT.is(error)) { + return [[...indicesErrors, error], timeRangeErrors, otherErrors]; + } else if (timeRangeValidationErrorRT.is(error)) { + return [indicesErrors, [...timeRangeErrors, error], otherErrors]; + } else { + return [indicesErrors, timeRangeErrors, [...otherErrors, error]]; + } + }, + [[], [], []] + ); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx index d69e544aeab18..4a3899f2d3918 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx @@ -4,15 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ValidationIndicesError } from '../../../../../common/http_api'; +import * as rt from 'io-ts'; +import { ValidationIndicesError, validationIndicesErrorRT } from '../../../../../common/http_api'; import { DatasetFilter } from '../../../../../common/log_analysis'; -export { ValidationIndicesError }; +export { ValidationIndicesError, validationIndicesErrorRT }; -export type ValidationIndicesUIError = +export const timeRangeValidationErrorRT = rt.strict({ + error: rt.literal('INVALID_TIME_RANGE'), +}); + +export type TimeRangeValidationError = rt.TypeOf; + +export type ValidationUIError = | ValidationIndicesError | { error: 'NETWORK_ERROR' } - | { error: 'TOO_FEW_SELECTED_INDICES' }; + | { error: 'TOO_FEW_SELECTED_INDICES' } + | TimeRangeValidationError; interface ValidAvailableIndex { validity: 'valid'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts index d46e8bc2485f6..9f757497aff81 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts @@ -16,7 +16,7 @@ import { import { AvailableIndex, ValidationIndicesError, - ValidationIndicesUIError, + ValidationUIError, } from '../../../components/logging/log_analysis_setup/initial_configuration_step'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; @@ -46,6 +46,11 @@ export const useAnalysisSetupState = ({ const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); + const isTimeRangeValid = useMemo( + () => (startTime != null && endTime != null ? startTime < endTime : true), + [endTime, startTime] + ); + const [validatedIndices, setValidatedIndices] = useState( sourceConfiguration.indices.map(indexName => ({ name: indexName, @@ -201,35 +206,54 @@ export const useAnalysisSetupState = ({ [validateDatasetsRequest.state, validateIndicesRequest.state] ); - const validationErrors = useMemo(() => { + const validationErrors = useMemo(() => { if (isValidating) { return []; } - if (validateIndicesRequest.state === 'rejected') { - return [{ error: 'NETWORK_ERROR' }]; - } - - if (selectedIndexNames.length === 0) { - return [{ error: 'TOO_FEW_SELECTED_INDICES' }]; - } - - return validatedIndices.reduce((errors, index) => { - return index.validity === 'invalid' && selectedIndexNames.includes(index.name) - ? [...errors, ...index.errors] - : errors; - }, []); - }, [isValidating, validateIndicesRequest.state, selectedIndexNames, validatedIndices]); + return [ + // validate request status + ...(validateIndicesRequest.state === 'rejected' || + validateDatasetsRequest.state === 'rejected' + ? [{ error: 'NETWORK_ERROR' as const }] + : []), + // validation request results + ...validatedIndices.reduce((errors, index) => { + return index.validity === 'invalid' && selectedIndexNames.includes(index.name) + ? [...errors, ...index.errors] + : errors; + }, []), + // index count + ...(selectedIndexNames.length === 0 ? [{ error: 'TOO_FEW_SELECTED_INDICES' as const }] : []), + // time range + ...(!isTimeRangeValid ? [{ error: 'INVALID_TIME_RANGE' as const }] : []), + ]; + }, [ + isValidating, + validateIndicesRequest.state, + validateDatasetsRequest.state, + validatedIndices, + selectedIndexNames, + isTimeRangeValid, + ]); const prevStartTime = usePrevious(startTime); const prevEndTime = usePrevious(endTime); const prevValidIndexNames = usePrevious(validIndexNames); useEffect(() => { + if (!isTimeRangeValid) { + return; + } + validateIndices(); - }, [validateIndices]); + }, [isTimeRangeValid, validateIndices]); useEffect(() => { + if (!isTimeRangeValid) { + return; + } + if ( startTime !== prevStartTime || endTime !== prevEndTime || @@ -239,6 +263,7 @@ export const useAnalysisSetupState = ({ } }, [ endTime, + isTimeRangeValid, prevEndTime, prevStartTime, prevValidIndexNames,