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,