[ML] DF Analytics creation wizard: resolve clone usability issues (#79048)

* show error when clone fails due to no index pattern

* default results_field unless specified

* results field switch set to on if resultsField for cloned job is default

* ensure cloned job config not overwritten in advanced editor

* show errorToast if unable to clone anomalyDetection job due to no indexPattern

* ensure jobConfig query getting saved in form state

* ensure index patterns with commas handled correctly

* clone should accept comma separated index patterns

* use nullish coalescing operator when checking for undefined analysisFields
This commit is contained in:
Melissa Alvarez 2020-10-06 13:29:16 -04:00 committed by GitHub
parent 287541891e
commit 06f87bb838
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 75 additions and 30 deletions

View file

@ -25,6 +25,8 @@ import { ANALYTICS_STEPS } from '../../page';
import { ml } from '../../../../../services/ml_api_service';
import { extractErrorMessage } from '../../../../../../../common/util/errors';
const DEFAULT_RESULTS_FIELD = 'ml';
const indexNameExistsMessage = i18n.translate(
'xpack.ml.dataframe.analytics.create.destinationIndexHelpText',
{
@ -64,6 +66,10 @@ export const DetailsStepForm: FC<CreateAnalyticsStepProps> = ({
const [destIndexSameAsId, setDestIndexSameAsId] = useState<boolean>(
cloneJob === undefined && hasSwitchedToEditor === false
);
const [useResultsFieldDefault, setUseResultsFieldDefault] = useState<boolean>(
(cloneJob === undefined && hasSwitchedToEditor === false && resultsField === undefined) ||
(cloneJob !== undefined && resultsField === DEFAULT_RESULTS_FIELD)
);
const forceInput = useRef<HTMLInputElement | null>(null);
@ -266,22 +272,46 @@ export const DetailsStepForm: FC<CreateAnalyticsStepProps> = ({
/>
</EuiFormRow>
)}
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.analytics.create.resultsFieldLabel', {
defaultMessage: 'Results field',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.resultsFieldHelpText', {
defaultMessage:
'Defines the name of the field in which to store the results of the analysis. Defaults to ml.',
})}
>
<EuiFieldText
<EuiFormRow fullWidth>
<EuiSwitch
disabled={isJobCreated}
value={resultsField}
onChange={(e) => setFormState({ resultsField: e.target.value })}
data-test-subj="mlAnalyticsCreateJobWizardResultsFieldInput"
name="mlDataFrameAnalyticsUseResultsFieldDefault"
label={i18n.translate('xpack.ml.dataframe.analytics.create.UseResultsFieldDefaultLabel', {
defaultMessage: 'Use results field default value "{defaultValue}"',
values: { defaultValue: DEFAULT_RESULTS_FIELD },
})}
checked={useResultsFieldDefault === true}
onChange={() => setUseResultsFieldDefault(!useResultsFieldDefault)}
data-test-subj="mlAnalyticsCreateJobWizardUseResultsFieldDefault"
/>
</EuiFormRow>
{useResultsFieldDefault === false && (
<EuiFormRow
fullWidth
label={i18n.translate('xpack.ml.dataframe.analytics.create.resultsFieldLabel', {
defaultMessage: 'Results field',
})}
helpText={i18n.translate('xpack.ml.dataframe.analytics.create.resultsFieldHelpText', {
defaultMessage:
'Defines the name of the field in which to store the results of the analysis. Defaults to ml.',
})}
>
<EuiFieldText
disabled={isJobCreated}
placeholder="results field"
value={resultsField}
onChange={(e) => setFormState({ resultsField: e.target.value })}
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.resultsFieldInputAriaLabel',
{
defaultMessage:
'The name of the field in which to store the results of the analysis.',
}
)}
data-test-subj="mlAnalyticsCreateJobWizardResultsFieldInput"
/>
</EuiFormRow>
)}
<EuiFormRow
fullWidth
isInvalid={

View file

@ -345,7 +345,7 @@ export const useNavigateToWizardWithClonedJob = () => {
return async (item: DataFrameAnalyticsListRow) => {
const sourceIndex = Array.isArray(item.config.source.index)
? item.config.source.index[0]
? item.config.source.index.join(',')
: item.config.source.index;
let sourceIndexId;
@ -363,6 +363,14 @@ export const useNavigateToWizardWithClonedJob = () => {
);
if (ip !== undefined) {
sourceIndexId = ip.id;
} else {
toasts.addDanger(
i18n.translate('xpack.ml.dataframe.analyticsList.noSourceIndexPatternForClone', {
defaultMessage:
'Unable to clone the analytics job. No index pattern exists for index {indexPattern}.',
values: { indexPattern: sourceIndex },
})
);
}
} catch (e) {
const error = extractErrorMessage(e);

View file

@ -24,9 +24,7 @@ export const useCloneAction = (canCreateDataFrameAnalytics: boolean) => {
const action: DataFrameAnalyticsListAction = useMemo(
() => ({
name: (item: DataFrameAnalyticsListRow) => (
<CloneActionName isDisabled={!canCreateDataFrameAnalytics} />
),
name: () => <CloneActionName isDisabled={!canCreateDataFrameAnalytics} />,
enabled: () => canCreateDataFrameAnalytics,
description: cloneActionNameText,
icon: 'copy',

View file

@ -549,8 +549,7 @@ export function reducer(state: State, action: Action): State {
}
case ACTION.SWITCH_TO_ADVANCED_EDITOR:
let { jobConfig } = state;
jobConfig = getJobConfigFromFormState(state.form);
const jobConfig = getJobConfigFromFormState(state.form);
const shouldDisableSwitchToForm = isAdvancedConfig(jobConfig);
return validateAdvancedEditor({

View file

@ -292,7 +292,6 @@ export function getFormStateFromJobConfig(
isClone: boolean = true
): Partial<State['form']> {
const jobType = getAnalysisType(analyticsJobConfig.analysis) as DataFrameAnalysisConfigType;
const resultState: Partial<State['form']> = {
jobType,
description: analyticsJobConfig.description ?? '',
@ -302,7 +301,8 @@ export function getFormStateFromJobConfig(
: analyticsJobConfig.source.index,
modelMemoryLimit: analyticsJobConfig.model_memory_limit,
maxNumThreads: analyticsJobConfig.max_num_threads,
includes: analyticsJobConfig.analyzed_fields.includes,
includes: analyticsJobConfig.analyzed_fields?.includes ?? [],
jobConfigQuery: analyticsJobConfig.source.query || defaultSearchQuery,
};
if (isClone === false) {

View file

@ -285,7 +285,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
resetForm();
const config = extractCloningConfig(cloneJob);
if (isAdvancedConfig(config)) {
setJobConfig(config);
setFormState(getFormStateFromJobConfig(config));
switchToAdvancedEditor();
} else {
setFormState(getFormStateFromJobConfig(config));

View file

@ -9,6 +9,7 @@ import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes';
import { getIndexPatternNames } from '../../../../util/index_utils';
import { stopDatafeeds, cloneJob, closeJobs, isStartable, isStoppable, isClosable } from '../utils';
import { getToastNotifications } from '../../../../util/dependency_cache';
import { i18n } from '@kbn/i18n';
export function actionsMenuContent(
@ -86,15 +87,24 @@ export function actionsMenuContent(
// the indexPattern the job was created for. An indexPattern could either have been deleted
// since the the job was created or the current user doesn't have the required permissions to
// access the indexPattern.
const indexPatternNames = getIndexPatternNames();
const jobIndicesAvailable = item.datafeedIndices.every((dfiName) => {
return indexPatternNames.some((ipName) => ipName === dfiName);
});
return item.deleting !== true && canCreateJob && jobIndicesAvailable;
return item.deleting !== true && canCreateJob;
},
onClick: (item) => {
cloneJob(item.id);
const indexPatternNames = getIndexPatternNames();
const indexPatternTitle = item.datafeedIndices.join(',');
const jobIndicesAvailable = indexPatternNames.includes(indexPatternTitle);
if (!jobIndicesAvailable) {
getToastNotifications().addDanger(
i18n.translate('xpack.ml.jobsList.managementActions.noSourceIndexPatternForClone', {
defaultMessage:
'Unable to clone the anomaly detection job {jobId}. No index pattern exists for index {indexPatternTitle}.',
values: { jobId: item.id, indexPatternTitle },
})
);
} else {
cloneJob(item.id);
}
closeMenu(true);
},
'data-test-subj': 'mlActionButtonCloneJob',

View file

@ -47,7 +47,7 @@ export const dataAnalyticsExplainSchema = schema.object({
dest: schema.maybe(schema.any()),
/** Source */
source: schema.object({
index: schema.string(),
index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
query: schema.maybe(schema.any()),
}),
analysis: schema.any(),