mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Fixing recognizer wizard input fields (#146325)
Fixes https://github.com/elastic/kibana/issues/144504 Removes the use of `usePartialState` which was causing an infinite render loop in favour of individual `useStates` for the input settings. Also contains a bit of code clean up, fixing hook dependencies and removing dependency cache use.
This commit is contained in:
parent
35e581534c
commit
9eee4c3d47
2 changed files with 188 additions and 174 deletions
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import React, { FC, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
|
@ -28,7 +28,6 @@ import {
|
|||
patternValidator,
|
||||
} from '../../../../../../common/util/validators';
|
||||
import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation';
|
||||
import { usePartialState } from '../../../../components/custom_hooks';
|
||||
import { TimeRange, TimeRangePicker } from '../../common/components';
|
||||
|
||||
export interface JobSettingsFormValues {
|
||||
|
@ -41,61 +40,62 @@ export interface JobSettingsFormValues {
|
|||
|
||||
interface JobSettingsFormProps {
|
||||
saveState: SAVE_STATE;
|
||||
onSubmit: (values: JobSettingsFormValues) => any;
|
||||
onChange: (values: JobSettingsFormValues) => any;
|
||||
onSubmit: (values: JobSettingsFormValues) => void;
|
||||
onJobPrefixChange: (jobPrefix: string) => void;
|
||||
jobs: ModuleJobUI[];
|
||||
}
|
||||
|
||||
export const JobSettingsForm: FC<JobSettingsFormProps> = ({
|
||||
onSubmit,
|
||||
onChange,
|
||||
onJobPrefixChange,
|
||||
saveState,
|
||||
jobs,
|
||||
}) => {
|
||||
const { from, to } = getTimeFilterRange();
|
||||
const { currentDataView: dataView } = useMlContext();
|
||||
|
||||
const jobPrefixValidator = composeValidators(
|
||||
patternValidator(/^([a-z0-9]+[a-z0-9\-_]*)?$/),
|
||||
maxLengthValidator(JOB_ID_MAX_LENGTH - Math.max(...jobs.map(({ id }) => id.length)))
|
||||
const jobPrefixValidator = useMemo(
|
||||
() =>
|
||||
composeValidators(
|
||||
patternValidator(/^([a-z0-9]+[a-z0-9\-_]*)?$/),
|
||||
maxLengthValidator(JOB_ID_MAX_LENGTH - Math.max(...jobs.map(({ id }) => id.length)))
|
||||
),
|
||||
[jobs]
|
||||
);
|
||||
|
||||
const [formState, setFormState] = usePartialState({
|
||||
jobPrefix: '',
|
||||
startDatafeedAfterSave: true,
|
||||
useFullIndexData: true,
|
||||
timeRange: {
|
||||
start: from,
|
||||
end: to,
|
||||
},
|
||||
useDedicatedIndex: false,
|
||||
const [jobPrefix, setJobPrefix] = useState('');
|
||||
const [startDatafeedAfterSave, setStartDatafeedAfterSave] = useState(true);
|
||||
const [useFullIndexData, setUseFullIndexData] = useState(true);
|
||||
const [timeRange, setTimeRange] = useState({
|
||||
start: from,
|
||||
end: to,
|
||||
});
|
||||
const [useDedicatedIndex, setUseDedicatedIndex] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState<Record<string, any>>({});
|
||||
|
||||
const onJobPrefixChange = (value: string) => {
|
||||
setFormState({
|
||||
jobPrefix: value && value.toLowerCase(),
|
||||
});
|
||||
const createJobSettings = () => {
|
||||
return {
|
||||
jobPrefix,
|
||||
startDatafeedAfterSave,
|
||||
useFullIndexData,
|
||||
timeRange,
|
||||
useDedicatedIndex,
|
||||
};
|
||||
};
|
||||
|
||||
const handleValidation = () => {
|
||||
const jobPrefixValidationResult = jobPrefixValidator(formState.jobPrefix);
|
||||
const handleValidation = useCallback(() => {
|
||||
const jobPrefixValidationResult = jobPrefixValidator(jobPrefix);
|
||||
|
||||
setValidationResult({
|
||||
jobPrefix: jobPrefixValidationResult,
|
||||
formValid: !jobPrefixValidationResult,
|
||||
});
|
||||
};
|
||||
}, [jobPrefix, jobPrefixValidator]);
|
||||
|
||||
useEffect(() => {
|
||||
handleValidation();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formState.jobPrefix]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(formState);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formState]);
|
||||
onJobPrefixChange(jobPrefix);
|
||||
}, [handleValidation, jobPrefix, onJobPrefixChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -150,8 +150,8 @@ export const JobSettingsForm: FC<JobSettingsFormProps> = ({
|
|||
>
|
||||
<EuiFieldText
|
||||
name="jobPrefix"
|
||||
value={formState.jobPrefix}
|
||||
onChange={({ target: { value } }) => onJobPrefixChange(value)}
|
||||
value={jobPrefix}
|
||||
onChange={({ target: { value } }) => setJobPrefix(value)}
|
||||
isInvalid={!!validationResult.jobPrefix}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -167,11 +167,9 @@ export const JobSettingsForm: FC<JobSettingsFormProps> = ({
|
|||
defaultMessage="Start datafeed after save"
|
||||
/>
|
||||
}
|
||||
checked={formState.startDatafeedAfterSave}
|
||||
checked={startDatafeedAfterSave}
|
||||
onChange={({ target: { checked } }) => {
|
||||
setFormState({
|
||||
startDatafeedAfterSave: checked,
|
||||
});
|
||||
setStartDatafeedAfterSave(checked);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -186,24 +184,20 @@ export const JobSettingsForm: FC<JobSettingsFormProps> = ({
|
|||
values={{ indexPatternTitle: dataView.title }}
|
||||
/>
|
||||
}
|
||||
checked={formState.useFullIndexData}
|
||||
checked={useFullIndexData}
|
||||
onChange={({ target: { checked } }) => {
|
||||
setFormState({
|
||||
useFullIndexData: checked,
|
||||
});
|
||||
setUseFullIndexData(checked);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{!formState.useFullIndexData && (
|
||||
{!useFullIndexData && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<TimeRangePicker
|
||||
setTimeRange={(value) => {
|
||||
setFormState({
|
||||
timeRange: value,
|
||||
});
|
||||
setTimeRange(value);
|
||||
}}
|
||||
timeRange={formState.timeRange}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -241,11 +235,9 @@ export const JobSettingsForm: FC<JobSettingsFormProps> = ({
|
|||
<EuiSwitch
|
||||
id="useDedicatedIndex"
|
||||
name="useDedicatedIndex"
|
||||
checked={formState.useDedicatedIndex}
|
||||
checked={useDedicatedIndex}
|
||||
onChange={({ target: { checked } }) => {
|
||||
setFormState({
|
||||
useDedicatedIndex: checked,
|
||||
});
|
||||
setUseDedicatedIndex(checked);
|
||||
}}
|
||||
label={i18n.translate('xpack.ml.newJob.recognize.useDedicatedIndexLabel', {
|
||||
defaultMessage: 'Use dedicated index',
|
||||
|
@ -263,7 +255,7 @@ export const JobSettingsForm: FC<JobSettingsFormProps> = ({
|
|||
isLoading={saveState === SAVE_STATE.SAVING}
|
||||
disabled={!validationResult.formValid || saveState === SAVE_STATE.SAVING}
|
||||
onClick={() => {
|
||||
onSubmit(formState);
|
||||
onSubmit(createJobSettings());
|
||||
}}
|
||||
aria-label={i18n.translate('xpack.ml.newJob.recognize.createJobButtonAriaLabel', {
|
||||
defaultMessage: 'Create job',
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, Fragment, useEffect } from 'react';
|
||||
import React, { FC, useState, Fragment, useEffect, useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
|
@ -17,11 +17,10 @@ import {
|
|||
EuiCallOut,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import { merge } from 'lodash';
|
||||
import { isEqual, merge } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
import { useMlKibana, useMlLocator } from '../../../contexts/kibana';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { useMlContext } from '../../../contexts/ml';
|
||||
import {
|
||||
DatafeedResponse,
|
||||
|
@ -73,7 +72,12 @@ export enum SAVE_STATE {
|
|||
|
||||
export const Page: FC<PageProps> = ({ moduleId, existingGroupIds }) => {
|
||||
const {
|
||||
services: { notifications },
|
||||
services: {
|
||||
notifications,
|
||||
mlServices: {
|
||||
mlApiServices: { getTimeFieldRange, setupDataRecognizerConfig, getDataRecognizerModule },
|
||||
},
|
||||
},
|
||||
} = useMlKibana();
|
||||
const locator = useMlLocator();
|
||||
|
||||
|
@ -109,9 +113,9 @@ export const Page: FC<PageProps> = ({ moduleId, existingGroupIds }) => {
|
|||
/**
|
||||
* Loads recognizer module configuration.
|
||||
*/
|
||||
const loadModule = async () => {
|
||||
const loadModule = useCallback(async () => {
|
||||
try {
|
||||
const response = await ml.getDataRecognizerModule({ moduleId });
|
||||
const response = await getDataRecognizerModule({ moduleId });
|
||||
setJobs(response.jobs);
|
||||
|
||||
const kibanaObjectsResult = await checkForSavedObjects(response.kibana as KibanaObjects);
|
||||
|
@ -121,137 +125,157 @@ export const Page: FC<PageProps> = ({ moduleId, existingGroupIds }) => {
|
|||
|
||||
// mix existing groups from the server with the groups used across all jobs in the module.
|
||||
const moduleGroups = [...response.jobs.map((j) => j.config.groups || [])].flat();
|
||||
setExistingGroups([...new Set([...existingGroups, ...moduleGroups])]);
|
||||
const newGroups = [...new Set([...existingGroups, ...moduleGroups])].sort();
|
||||
if (!isEqual(newGroups, existingGroups)) {
|
||||
setExistingGroups(newGroups);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
}, [existingGroups, getDataRecognizerModule, moduleId]);
|
||||
|
||||
const getTimeRange = async (
|
||||
useFullIndexData: boolean,
|
||||
timeRange: TimeRange
|
||||
): Promise<TimeRange> => {
|
||||
if (useFullIndexData) {
|
||||
const runtimeMappings = dataView.getComputedFields().runtimeFields as RuntimeMappings;
|
||||
const { start, end } = await ml.getTimeFieldRange({
|
||||
index: dataView.title,
|
||||
timeFieldName: dataView.timeFieldName,
|
||||
// By default we want to use full non-frozen time range
|
||||
query: addExcludeFrozenToQuery(combinedQuery),
|
||||
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
|
||||
});
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
};
|
||||
} else {
|
||||
return Promise.resolve(timeRange);
|
||||
}
|
||||
};
|
||||
const getTimeRange = useCallback(
|
||||
async (useFullIndexData: boolean, timeRange: TimeRange): Promise<TimeRange> => {
|
||||
if (useFullIndexData) {
|
||||
const runtimeMappings = dataView.getComputedFields().runtimeFields as RuntimeMappings;
|
||||
const { start, end } = await getTimeFieldRange({
|
||||
index: dataView.title,
|
||||
timeFieldName: dataView.timeFieldName,
|
||||
// By default we want to use full non-frozen time range
|
||||
query: addExcludeFrozenToQuery(combinedQuery),
|
||||
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
|
||||
});
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
};
|
||||
} else {
|
||||
return Promise.resolve(timeRange);
|
||||
}
|
||||
},
|
||||
[combinedQuery, dataView, getTimeFieldRange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadModule();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [loadModule]);
|
||||
|
||||
/**
|
||||
* Sets up recognizer module configuration.
|
||||
*/
|
||||
const save = async (formValues: JobSettingsFormValues) => {
|
||||
setSaveState(SAVE_STATE.SAVING);
|
||||
const {
|
||||
jobPrefix: resultJobPrefix,
|
||||
startDatafeedAfterSave,
|
||||
useDedicatedIndex,
|
||||
useFullIndexData,
|
||||
timeRange,
|
||||
} = formValues;
|
||||
|
||||
const resultTimeRange = await getTimeRange(useFullIndexData, timeRange);
|
||||
|
||||
try {
|
||||
let jobOverridesPayload: JobOverride[] | null = Object.values(jobOverrides);
|
||||
jobOverridesPayload = jobOverridesPayload.length > 0 ? jobOverridesPayload : null;
|
||||
|
||||
const response = await ml.setupDataRecognizerConfig({
|
||||
moduleId,
|
||||
prefix: resultJobPrefix,
|
||||
query: tempQuery,
|
||||
indexPatternName: dataView.title,
|
||||
const save = useCallback(
|
||||
async (formValues: JobSettingsFormValues) => {
|
||||
setSaveState(SAVE_STATE.SAVING);
|
||||
const {
|
||||
jobPrefix: resultJobPrefix,
|
||||
startDatafeedAfterSave,
|
||||
useDedicatedIndex,
|
||||
startDatafeed: startDatafeedAfterSave,
|
||||
...(jobOverridesPayload !== null ? { jobOverrides: jobOverridesPayload } : {}),
|
||||
...resultTimeRange,
|
||||
});
|
||||
const { datafeeds: datafeedsResponse, jobs: jobsResponse, kibana: kibanaResponse } = response;
|
||||
useFullIndexData,
|
||||
timeRange,
|
||||
} = formValues;
|
||||
|
||||
setJobs(
|
||||
jobs.map((job) => {
|
||||
return {
|
||||
...job,
|
||||
datafeedResult: datafeedsResponse.find(({ id }) => id.endsWith(job.id)),
|
||||
setupResult: jobsResponse.find(({ id }) => id === resultJobPrefix + job.id),
|
||||
};
|
||||
})
|
||||
);
|
||||
setKibanaObjects(merge(kibanaObjects, kibanaResponse));
|
||||
const resultTimeRange = await getTimeRange(useFullIndexData, timeRange);
|
||||
|
||||
if (locator) {
|
||||
const url = await locator.getUrl({
|
||||
page: ML_PAGES.ANOMALY_EXPLORER,
|
||||
pageState: {
|
||||
jobIds: jobsResponse.filter(({ success }) => success).map(({ id }) => id),
|
||||
timeRange: {
|
||||
from: moment(resultTimeRange.start).format(TIME_FORMAT),
|
||||
to: moment(resultTimeRange.end).format(TIME_FORMAT),
|
||||
mode: 'absolute',
|
||||
},
|
||||
},
|
||||
try {
|
||||
let jobOverridesPayload: JobOverride[] | null = Object.values(jobOverrides);
|
||||
jobOverridesPayload = jobOverridesPayload.length > 0 ? jobOverridesPayload : null;
|
||||
|
||||
const response = await setupDataRecognizerConfig({
|
||||
moduleId,
|
||||
prefix: resultJobPrefix,
|
||||
query: tempQuery,
|
||||
indexPatternName: dataView.title,
|
||||
useDedicatedIndex,
|
||||
startDatafeed: startDatafeedAfterSave,
|
||||
...(jobOverridesPayload !== null ? { jobOverrides: jobOverridesPayload } : {}),
|
||||
...resultTimeRange,
|
||||
});
|
||||
const {
|
||||
datafeeds: datafeedsResponse,
|
||||
jobs: jobsResponse,
|
||||
kibana: kibanaResponse,
|
||||
} = response;
|
||||
|
||||
setJobs(
|
||||
jobs.map((job) => {
|
||||
return {
|
||||
...job,
|
||||
datafeedResult: datafeedsResponse.find(({ id }) => id.endsWith(job.id)),
|
||||
setupResult: jobsResponse.find(({ id }) => id === resultJobPrefix + job.id),
|
||||
};
|
||||
})
|
||||
);
|
||||
setKibanaObjects(merge(kibanaObjects, kibanaResponse));
|
||||
|
||||
if (locator) {
|
||||
const url = await locator.getUrl({
|
||||
page: ML_PAGES.ANOMALY_EXPLORER,
|
||||
pageState: {
|
||||
jobIds: jobsResponse.filter(({ success }) => success).map(({ id }) => id),
|
||||
timeRange: {
|
||||
from: moment(resultTimeRange.start).format(TIME_FORMAT),
|
||||
to: moment(resultTimeRange.end).format(TIME_FORMAT),
|
||||
mode: 'absolute',
|
||||
},
|
||||
},
|
||||
});
|
||||
setResultsUrl(url);
|
||||
}
|
||||
|
||||
const failedJobsCount = jobsResponse.reduce(
|
||||
(count, { success }) => (success ? count : count + 1),
|
||||
0
|
||||
);
|
||||
|
||||
const lazyJobsCount = datafeedsResponse.reduce(
|
||||
(count, { awaitingMlNodeAllocation }) =>
|
||||
awaitingMlNodeAllocation === true ? count + 1 : count,
|
||||
0
|
||||
);
|
||||
|
||||
setJobsAwaitingNodeCount(lazyJobsCount);
|
||||
|
||||
setSaveState(
|
||||
failedJobsCount === 0
|
||||
? SAVE_STATE.SAVED
|
||||
: failedJobsCount === jobs.length
|
||||
? SAVE_STATE.FAILED
|
||||
: SAVE_STATE.PARTIAL_FAILURE
|
||||
);
|
||||
} catch (e) {
|
||||
setSaveState(SAVE_STATE.FAILED);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error setting up module', e);
|
||||
const { toasts } = notifications;
|
||||
toasts.addDanger({
|
||||
title: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningTitle', {
|
||||
defaultMessage: 'Error setting up module {moduleId}',
|
||||
values: { moduleId },
|
||||
}),
|
||||
text: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningDescription', {
|
||||
defaultMessage:
|
||||
'An error occurred trying to create the {count, plural, one {job} other {jobs}} in the module.',
|
||||
values: {
|
||||
count: jobs.length,
|
||||
},
|
||||
}),
|
||||
});
|
||||
setResultsUrl(url);
|
||||
}
|
||||
|
||||
const failedJobsCount = jobsResponse.reduce(
|
||||
(count, { success }) => (success ? count : count + 1),
|
||||
0
|
||||
);
|
||||
|
||||
const lazyJobsCount = datafeedsResponse.reduce(
|
||||
(count, { awaitingMlNodeAllocation }) =>
|
||||
awaitingMlNodeAllocation === true ? count + 1 : count,
|
||||
0
|
||||
);
|
||||
|
||||
setJobsAwaitingNodeCount(lazyJobsCount);
|
||||
|
||||
setSaveState(
|
||||
failedJobsCount === 0
|
||||
? SAVE_STATE.SAVED
|
||||
: failedJobsCount === jobs.length
|
||||
? SAVE_STATE.FAILED
|
||||
: SAVE_STATE.PARTIAL_FAILURE
|
||||
);
|
||||
} catch (e) {
|
||||
setSaveState(SAVE_STATE.FAILED);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error setting up module', e);
|
||||
const { toasts } = notifications;
|
||||
toasts.addDanger({
|
||||
title: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningTitle', {
|
||||
defaultMessage: 'Error setting up module {moduleId}',
|
||||
values: { moduleId },
|
||||
}),
|
||||
text: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningDescription', {
|
||||
defaultMessage:
|
||||
'An error occurred trying to create the {count, plural, one {job} other {jobs}} in the module.',
|
||||
values: {
|
||||
count: jobs.length,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
dataView.title,
|
||||
getTimeRange,
|
||||
jobOverrides,
|
||||
jobs,
|
||||
kibanaObjects,
|
||||
locator,
|
||||
moduleId,
|
||||
notifications,
|
||||
setupDataRecognizerConfig,
|
||||
tempQuery,
|
||||
]
|
||||
);
|
||||
|
||||
const onJobOverridesChange = (job: JobOverride) => {
|
||||
setJobOverrides({
|
||||
|
@ -321,9 +345,7 @@ export const Page: FC<PageProps> = ({ moduleId, existingGroupIds }) => {
|
|||
{isFormVisible && (
|
||||
<JobSettingsForm
|
||||
onSubmit={save}
|
||||
onChange={(formValues) => {
|
||||
setJobPrefix(formValues.jobPrefix);
|
||||
}}
|
||||
onJobPrefixChange={setJobPrefix}
|
||||
saveState={saveState}
|
||||
jobs={jobs}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue