[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:
parent
287541891e
commit
06f87bb838
|
@ -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={
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in a new issue