[7.5] [Logs UI] Provide index name pattern choice during ML jo… (#48849)

Backports the following commits to 7.5:
 - [Logs UI] Provide index name pattern choice during ML job setup (#48231)
This commit is contained in:
Felix Stürmer 2019-10-22 12:15:10 +02:00 committed by GitHub
parent 32f12a8397
commit 094969b5ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 527 additions and 294 deletions

View file

@ -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);

View file

@ -11,6 +11,11 @@ export type Pick3<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyo
[P1 in K1]: { [P2 in K2]: { [P3 in K3]: ((T[K1])[K2])[P3] } };
};
export type MandatoryProperty<T, Prop extends keyof T> = T &
{
[prop in Prop]-?: NonNullable<T[Prop]>;
};
/**
* Portions of below code are derived from https://github.com/tycho01/typical
* under the MIT License

View file

@ -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<typeof setupMlModuleResponsePayloadRT>;

View file

@ -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(',');
};

View file

@ -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<string, boolean>;
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<number | undefined>(Date.now() - fourWeeksInMs);
const [endTime, setEndTime] = useState<number | undefined>(undefined);
const [selectedIndices, setSelectedIndices] = useState<IndicesSelection>(
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,
};
};

View file

@ -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<JobType, JobStatus>;
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 extends any>(value: Value): value is MandatoryProperty<Value, 'error'> =>
value.error != null;
export const useStatusState = (sourceConfiguration: JobSourceConfiguration) => {
return useReducer(statusReducer, sourceConfiguration, createInitialState);
};

View file

@ -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 (
<AnalysisSetupContent
setup={setup}
availableIndices={availableIndices}
cleanupAndSetup={cleanupAndSetup}
errorMessages={lastSetupErrorMessages}
setup={setup}
setupStatus={setupStatus}
indexPattern={source ? source.configuration.logAlias : ''}
viewResults={viewResults}
/>
);

View file

@ -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<AnalysisSetupContentProps> = ({
availableIndices,
cleanupAndSetup,
indexPattern,
errorMessages,
setup,
setupStatus,
viewResults,
@ -71,11 +77,12 @@ export const AnalysisSetupContent: React.FunctionComponent<AnalysisSetupContentP
</EuiText>
<EuiSpacer />
<AnalysisSetupSteps
setup={setup}
availableIndices={availableIndices}
cleanupAndSetup={cleanupAndSetup}
viewResults={viewResults}
indexPattern={indexPattern}
errorMessages={errorMessages}
setup={setup}
setupStatus={setupStatus}
viewResults={viewResults}
/>
</EuiPageContentBody>
</AnalysisPageContent>
@ -86,7 +93,7 @@ export const AnalysisSetupContent: React.FunctionComponent<AnalysisSetupContentP
// !important due to https://github.com/elastic/eui/issues/2232
const AnalysisPageContent = euiStyled(EuiPageContent)`
max-width: 518px !important;
max-width: 768px !important;
`;
const AnalysisSetupPage = euiStyled(EuiPage)`

View file

@ -1,134 +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, { useMemo } from 'react';
import moment, { Moment } from 'moment';
import { i18n } from '@kbn/i18n';
import {
EuiForm,
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 (
<EuiForm>
<EuiDescribedFormGroup
idAria="timeRange"
title={
<FormattedMessage
id="xpack.infra.analysisSetup.timeRangeTitle"
defaultMessage="Choose a time range"
/>
}
description={
<FormattedMessage
id="xpack.infra.analysisSetup.timeRangeDescription"
defaultMessage="By default, Machine Learning analyzes log messages in your log indices no older than four weeks, and continues indefinitely. You can specify a different date to begin, to end, or both."
/>
}
>
<EuiFormRow
describedByIds={['timeRange']}
error={false}
fullWidth
isInvalid={false}
label={startTimeLabel}
>
<EuiFlexGroup gutterSize="s">
<EuiFormControlLayout
clear={startTime ? { onClick: () => setStartTime(undefined) } : undefined}
>
<EuiDatePicker
showTimeSelect
selected={startTimeValue}
onChange={date => setStartTime(selectedDateToParam(date))}
placeholder={startTimeDefaultDescription}
maxDate={now}
/>
</EuiFormControlLayout>
</EuiFlexGroup>
</EuiFormRow>
<EuiFormRow
describedByIds={['timeRange']}
error={false}
fullWidth
isInvalid={false}
label={endTimeLabel}
>
<EuiFlexGroup gutterSize="s">
<EuiFormControlLayout
clear={endTime ? { onClick: () => setEndTime(undefined) } : undefined}
>
<EuiDatePicker
showTimeSelect
selected={endTimeValue}
onChange={date => setEndTime(selectedDateToParam(date))}
placeholder={endTimeDefaultDescription}
openToDate={now}
minDate={startTimeValue}
minTime={
selectedEndTimeIsToday
? now
: moment()
.hour(0)
.minutes(0)
}
maxTime={moment()
.hour(23)
.minutes(59)}
/>
</EuiFormControlLayout>
</EuiFlexGroup>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiForm>
);
};

View file

@ -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';

View file

@ -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<string, boolean>;
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: <EuiCode>{indexName}</EuiCode>,
})),
[indices]
);
const handleCheckboxGroupChange = useCallback(
indexName => {
onChangeSelectedIndices({
...indices,
[indexName]: !indices[indexName],
});
},
[indices, onChangeSelectedIndices]
);
return (
<EuiDescribedFormGroup
idAria="indices"
title={
<FormattedMessage
id="xpack.infra.analysisSetup.indicesSelectionTitle"
defaultMessage="Choose indices"
/>
}
description={
<FormattedMessage
id="xpack.infra.analysisSetup.indicesSelectionDescription"
defaultMessage="By default, Machine Learning analyzes log messages in all log indices configured for the source. You can choose to only analyze a subset of the index names. Every selected index name must match at least one index with log entries."
/>
}
>
<EuiFormRow
describedByIds={['indices']}
error={validationErrors.map(formatValidationError)}
fullWidth
isInvalid={validationErrors.length > 0}
label={indicesSelectionLabel}
labelType="legend"
>
<EuiCheckboxGroup
options={choices}
idToSelectedMap={indices}
onChange={handleCheckboxGroupChange}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
};
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.',
}
);
}
};

View file

@ -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 (
<EuiDescribedFormGroup
idAria="timeRange"
title={
<FormattedMessage
id="xpack.infra.analysisSetup.timeRangeTitle"
defaultMessage="Choose a time range"
/>
}
description={
<FormattedMessage
id="xpack.infra.analysisSetup.timeRangeDescription"
defaultMessage="By default, Machine Learning analyzes log messages in your log indices no older than four weeks, and continues indefinitely. You can specify a different date to begin, to end, or both."
/>
}
>
<EuiFormRow
describedByIds={['timeRange']}
error={false}
fullWidth
isInvalid={false}
label={startTimeLabel}
>
<EuiFlexGroup gutterSize="s">
<EuiFormControlLayout
clear={startTime ? { onClick: () => setStartTime(undefined) } : undefined}
>
<EuiDatePicker
showTimeSelect
selected={startTimeValue}
onChange={date => setStartTime(selectedDateToParam(date))}
placeholder={startTimeDefaultDescription}
maxDate={now}
/>
</EuiFormControlLayout>
</EuiFlexGroup>
</EuiFormRow>
<EuiFormRow
describedByIds={['timeRange']}
error={false}
fullWidth
isInvalid={false}
label={endTimeLabel}
>
<EuiFlexGroup gutterSize="s">
<EuiFormControlLayout
clear={endTime ? { onClick: () => setEndTime(undefined) } : undefined}
>
<EuiDatePicker
showTimeSelect
selected={endTimeValue}
onChange={date => setEndTime(selectedDateToParam(date))}
placeholder={endTimeDefaultDescription}
openToDate={now}
minDate={startTimeValue}
minTime={
selectedEndTimeIsToday
? now
: moment()
.hour(0)
.minutes(0)
}
maxTime={moment()
.hour(23)
.minutes(59)}
/>
</EuiFormControlLayout>
</EuiFlexGroup>
</EuiFormRow>
</EuiDescribedFormGroup>
);
};

View file

@ -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';

View file

@ -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<InitialConfigurationStepProps> = ({
setStartTime,
setEndTime,
startTime,
endTime,
selectedIndices,
setSelectedIndices,
validationErrors = [],
}: InitialConfigurationStepProps) => {
const indicesFormValidationErrors = useMemo(
() =>
validationErrors.filter(validationError => validationError === 'TOO_FEW_SELECTED_INDICES'),
[validationErrors]
);
return (
<>
<EuiSpacer size="m" />
<EuiForm>
<AnalysisSetupTimerangeForm
setStartTime={setStartTime}
setEndTime={setEndTime}
startTime={startTime}
endTime={endTime}
/>
<AnalysisSetupIndicesForm
indices={selectedIndices}
onChangeSelectedIndices={setSelectedIndices}
validationErrors={indicesFormValidationErrors}
/>
</EuiForm>
</>
);
};

View file

@ -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 (
<EuiButton fill onClick={onClick}>
<EuiButton isDisabled={isDisabled} fill onClick={onClick}>
<FormattedMessage
id="xpack.infra.analysisSetup.createMlJobButton"
defaultMessage="Create ML job"

View file

@ -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 './process_step';

View file

@ -11,29 +11,34 @@ import {
EuiLoadingSpinner,
EuiSpacer,
EuiText,
EuiCallOut,
EuiCode,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { SetupStatus } from '../../../../../../common/log_analysis';
import { CreateMLJobsButton } from '../create_ml_jobs_button';
import { RecreateMLJobsButton } from '../recreate_ml_jobs_button';
import { CreateMLJobsButton } from './create_ml_jobs_button';
import { RecreateMLJobsButton } from './recreate_ml_jobs_button';
interface Props {
viewResults: () => void;
setup: () => void;
interface ProcessStepProps {
cleanupAndSetup: () => void;
indexPattern: string;
errorMessages: string[];
isConfigurationValid: boolean;
setup: () => void;
setupStatus: SetupStatus;
viewResults: () => void;
}
export const SetupProcess: React.FunctionComponent<Props> = ({
viewResults,
setup,
export const ProcessStep: React.FunctionComponent<ProcessStepProps> = ({
cleanupAndSetup,
indexPattern,
errorMessages,
isConfigurationValid,
setup,
setupStatus,
}: Props) => {
viewResults,
}) => {
return (
<EuiText size="s">
{setupStatus === 'pending' ? (
@ -52,13 +57,15 @@ export const SetupProcess: React.FunctionComponent<Props> = ({
<>
<FormattedMessage
id="xpack.infra.analysisSetup.steps.setupProcess.failureText"
defaultMessage="Something went wrong creating the necessary ML jobs.
Please ensure your configured logs indices ({indexPattern}) exist."
values={{
indexPattern,
}}
defaultMessage="Something went wrong creating the necessary ML jobs. Please ensure all selected log indices exist."
/>
<EuiSpacer />
{errorMessages.map(errorMessage => (
<EuiCallOut color="danger" iconType="alert" title={errorCalloutTitle}>
<EuiCode transparentBackground>{errorMessage}</EuiCode>
</EuiCallOut>
))}
<EuiSpacer />
<EuiButton fill onClick={cleanupAndSetup}>
<FormattedMessage
id="xpack.infra.analysisSetup.steps.setupProcess.tryAgainButton"
@ -81,10 +88,17 @@ export const SetupProcess: React.FunctionComponent<Props> = ({
</EuiButton>
</>
) : setupStatus === 'requiredForUpdate' || setupStatus === 'requiredForReconfiguration' ? (
<RecreateMLJobsButton onClick={cleanupAndSetup} />
<RecreateMLJobsButton isDisabled={!isConfigurationValid} onClick={cleanupAndSetup} />
) : (
<CreateMLJobsButton onClick={setup} />
<CreateMLJobsButton isDisabled={!isConfigurationValid} onClick={setup} />
)}
</EuiText>
);
};
const errorCalloutTitle = i18n.translate(
'xpack.infra.analysisSetup.steps.setupProcess.errorCalloutTitle',
{
defaultMessage: 'An error occurred',
}
);

View file

@ -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 (
<>
<FormattedMessage
@ -18,7 +19,7 @@ export const RecreateMLJobsButton: React.FunctionComponent<{
defaultMessage="This removes previously detected anomalies."
tagName="p"
/>
<EuiButton fill onClick={onClick}>
<EuiButton isDisabled={isDisabled} fill onClick={onClick}>
<FormattedMessage
id="xpack.infra.analysisSetup.recreateMlJobButton"
defaultMessage="Recreate ML jobs"

View file

@ -8,24 +8,30 @@ import { EuiSteps, EuiStepStatus } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { SetupStatus } from '../../../../../../common/log_analysis';
import { useAnalysisSetupState } from '../../../../../containers/logs/log_analysis/log_analysis_setup_state';
import { InitialConfiguration } from './initial_configuration';
import { SetupProcess } from './setup_process';
import { SetupStatus } from '../../../../../common/log_analysis';
import { useAnalysisSetupState } from '../../../../containers/logs/log_analysis/log_analysis_setup_state';
import { InitialConfigurationStep } from './initial_configuration_step';
import { ProcessStep } from './process_step';
type SetupHandler = (startTime?: number | undefined, endTime?: number | undefined) => 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<AnalysisSetupStepsProps> = ({
availableIndices,
cleanupAndSetup: cleanupAndSetupModule,
indexPattern,
errorMessages,
setup: setupModule,
setupStatus,
viewResults,
@ -37,36 +43,44 @@ export const AnalysisSetupSteps: React.FunctionComponent<AnalysisSetupStepsProps
setEndTime,
startTime,
endTime,
selectedIndices,
setSelectedIndices,
validationErrors,
} = useAnalysisSetupState({
availableIndices,
setupModule,
cleanupAndSetupModule,
});
const steps = [
{
title: i18n.translate('xpack.infra.analysisSetup.stepOneTitle', {
defaultMessage: 'Configuration (optional)',
title: i18n.translate('xpack.infra.analysisSetup.configurationStepTitle', {
defaultMessage: 'Configuration',
}),
children: (
<InitialConfiguration
<InitialConfigurationStep
setStartTime={setStartTime}
setEndTime={setEndTime}
startTime={startTime}
endTime={endTime}
selectedIndices={selectedIndices}
setSelectedIndices={setSelectedIndices}
validationErrors={validationErrors}
/>
),
},
{
title: i18n.translate('xpack.infra.analysisSetup.stepTwoTitle', {
title: i18n.translate('xpack.infra.analysisSetup.actionStepTitle', {
defaultMessage: 'Create ML job',
}),
children: (
<SetupProcess
<ProcessStep
cleanupAndSetup={cleanupAndSetup}
errorMessages={errorMessages}
isConfigurationValid={validationErrors.length <= 0}
setup={setup}
setupStatus={setupStatus}
viewResults={viewResults}
setup={setup}
cleanupAndSetup={cleanupAndSetup}
indexPattern={indexPattern}
/>
),
status:

View file

@ -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<InitialConfigurationProps> = ({
setStartTime,
setEndTime,
startTime,
endTime,
}: InitialConfigurationProps) => {
const [showTimeRangeForm, setShowTimeRangeForm] = useState(false);
return (
<>
{showTimeRangeForm ? (
<>
<EuiSpacer size="l" />
<AnalysisSetupTimerangeForm
setStartTime={setStartTime}
setEndTime={setEndTime}
startTime={startTime}
endTime={endTime}
/>
</>
) : (
<>
<EuiSpacer size="m" />
<EuiText size="s">
<FormattedMessage
id="xpack.infra.analysisSetup.timeRangeByDefault"
defaultMessage="By default, Machine Learning analyzes log messages in your log indices no older than four weeks, and continues indefinitely."
/>{' '}
<EuiLink
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setShowTimeRangeForm(true);
}}
>
<FormattedMessage
id="xpack.infra.analysisSetup.configureTimeRange"
defaultMessage="Configure time range?"
/>
</EuiLink>
</EuiText>
</>
)}
</>
);
};

View file

@ -5212,13 +5212,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": "このチャートはデータが欠けています。",

View file

@ -5215,13 +5215,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": "此图表的数据缺失。",