diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 8ab45dc24aa17..440585dcf2a19 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -268,8 +268,13 @@ export function resetJob(jobCreator: JobCreatorType, navigateToPath: NavigateToP navigateToPath('/jobs/new_job'); } -export function advancedStartDatafeed(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { - stashCombinedJob(jobCreator, false, false); +export function advancedStartDatafeed( + jobCreator: JobCreatorType | null, + navigateToPath: NavigateToPath +) { + if (jobCreator !== null) { + stashCombinedJob(jobCreator, false, false); + } navigateToPath('/jobs'); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/start_datafeed_switch/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/start_datafeed_switch/index.ts new file mode 100644 index 0000000000000..a62e378222ff9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/start_datafeed_switch/index.ts @@ -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 { StartDatafeedSwitch } from './start_datafeed_switch'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/start_datafeed_switch/start_datafeed_switch.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/start_datafeed_switch/start_datafeed_switch.tsx new file mode 100644 index 0000000000000..4aa78cfc41009 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/start_datafeed_switch/start_datafeed_switch.tsx @@ -0,0 +1,44 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSwitch, EuiFormRow, EuiSpacer } from '@elastic/eui'; +interface Props { + startDatafeed: boolean; + setStartDatafeed(start: boolean): void; + disabled?: boolean; +} + +export const StartDatafeedSwitch: FC = ({ + startDatafeed, + setStartDatafeed, + disabled = false, +}) => { + return ( + <> + + + setStartDatafeed(e.target.checked)} + disabled={disabled} + /> + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 3000ce8449138..669b8837e74b5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -28,6 +28,7 @@ import { DatafeedDetails } from './components/datafeed_details'; import { DetectorChart } from './components/detector_chart'; import { JobProgress } from './components/job_progress'; import { PostSaveOptions } from './components/post_save_options'; +import { StartDatafeedSwitch } from './components/start_datafeed_switch'; import { toastNotificationServiceProvider } from '../../../../../services/toast_notification_service'; import { convertToAdvancedJob, @@ -50,6 +51,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const [creatingJob, setCreatingJob] = useState(false); const [isValid, setIsValid] = useState(jobValidator.validationSummary.basic); const [jobRunner, setJobRunner] = useState(null); + const [startDatafeed, setStartDatafeed] = useState(true); const isAdvanced = isAdvancedJobCreator(jobCreator); const jsonEditorMode = isAdvanced ? EDITOR_MODE.EDITABLE : EDITOR_MODE.READONLY; @@ -59,15 +61,17 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => }, []); async function start() { + setCreatingJob(true); if (isAdvanced) { - await startAdvanced(); + await createAdvancedJob(); + } else if (startDatafeed === true) { + await createAndStartJob(); } else { - await startInline(); + await createAdvancedJob(false); } } - async function startInline() { - setCreatingJob(true); + async function createAndStartJob() { try { const jr = await jobCreator.createAndStartJob(); setJobRunner(jr); @@ -76,12 +80,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => } } - async function startAdvanced() { - setCreatingJob(true); + async function createAdvancedJob(showStartModal: boolean = true) { try { await jobCreator.createJob(); await jobCreator.createDatafeed(); - advancedStartDatafeed(jobCreator, navigateToPath); + advancedStartDatafeed(showStartModal ? jobCreator : null, navigateToPath); } catch (error) { handleJobCreationError(error); } @@ -131,6 +134,14 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => + {isAdvanced === false && ( + + )} + {isAdvanced && ( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts index 2524d0486171b..d3de41689244b 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts @@ -108,7 +108,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job creation displays the time range step'); await ml.jobWizardCommon.assertTimeRangeSectionExists(); - await ml.testExecution.logTestStep('job creation sets the timerange'); + await ml.testExecution.logTestStep('job creation sets the time range'); await ml.jobWizardCommon.clickUseFullDataButton( 'Apr 5, 2019 @ 11:25:35.770', 'Nov 21, 2019 @ 06:01:13.914' @@ -230,7 +230,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job cloning displays the time range step'); await ml.jobWizardCommon.assertTimeRangeSectionExists(); - await ml.testExecution.logTestStep('job cloning sets the timerange'); + await ml.testExecution.logTestStep('job cloning sets the time range'); await ml.jobWizardCommon.clickUseFullDataButton( 'Apr 5, 2019 @ 11:25:35.770', 'Nov 21, 2019 @ 06:01:13.914' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts index 0983ebd79dd90..07ffef62ce3c0 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts @@ -10,6 +10,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags(['skipFirefox']); loadTestFile(require.resolve('./single_metric_job')); + loadTestFile(require.resolve('./single_metric_job_without_datafeed_start')); loadTestFile(require.resolve('./multi_metric_job')); loadTestFile(require.resolve('./population_job')); loadTestFile(require.resolve('./saved_search_job')); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts index 5324890b269bc..0964be83f628b 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts @@ -104,7 +104,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job creation displays the time range step'); await ml.jobWizardCommon.assertTimeRangeSectionExists(); - await ml.testExecution.logTestStep('job creation sets the timerange'); + await ml.testExecution.logTestStep('job creation sets the time range'); await ml.jobWizardCommon.clickUseFullDataButton( 'Feb 7, 2016 @ 00:00:00.000', 'Feb 11, 2016 @ 23:59:54.000' @@ -235,7 +235,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job cloning displays the time range step'); await ml.jobWizardCommon.assertTimeRangeSectionExists(); - await ml.testExecution.logTestStep('job cloning sets the timerange'); + await ml.testExecution.logTestStep('job cloning sets the time range'); await ml.jobWizardCommon.clickUseFullDataButton( 'Feb 7, 2016 @ 00:00:00.000', 'Feb 11, 2016 @ 23:59:54.000' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts index 4797334ee57af..6b914f0c2ba07 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts @@ -118,7 +118,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job creation displays the time range step'); await ml.jobWizardCommon.assertTimeRangeSectionExists(); - await ml.testExecution.logTestStep('job creation sets the timerange'); + await ml.testExecution.logTestStep('job creation sets the time range'); await ml.jobWizardCommon.clickUseFullDataButton( 'Jun 12, 2019 @ 00:04:19.000', 'Jul 12, 2019 @ 23:45:36.000' @@ -261,7 +261,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job cloning displays the time range step'); await ml.jobWizardCommon.assertTimeRangeSectionExists(); - await ml.testExecution.logTestStep('job cloning sets the timerange'); + await ml.testExecution.logTestStep('job cloning sets the time range'); await ml.jobWizardCommon.clickUseFullDataButton( 'Jun 12, 2019 @ 00:04:19.000', 'Jul 12, 2019 @ 23:45:36.000' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts index ea3a42e2f27c8..f5f416411f175 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts @@ -306,7 +306,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job creation displays the time range step'); await ml.jobWizardCommon.assertTimeRangeSectionExists(); - await ml.testExecution.logTestStep('job creation sets the timerange'); + await ml.testExecution.logTestStep('job creation sets the time range'); await ml.jobWizardCommon.clickUseFullDataButton( 'Feb 7, 2016 @ 00:00:00.000', 'Feb 11, 2016 @ 23:59:54.000' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts index 89612e51eee13..dd6310b67d844 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts @@ -103,7 +103,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job creation displays the time range step'); await ml.jobWizardCommon.assertTimeRangeSectionExists(); - await ml.testExecution.logTestStep('job creation sets the timerange'); + await ml.testExecution.logTestStep('job creation sets the time range'); await ml.jobWizardCommon.clickUseFullDataButton( 'Feb 7, 2016 @ 00:00:00.000', 'Feb 11, 2016 @ 23:59:54.000' @@ -212,7 +212,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job cloning displays the time range step'); await ml.jobWizardCommon.assertTimeRangeSectionExists(); - await ml.testExecution.logTestStep('job cloning sets the timerange'); + await ml.testExecution.logTestStep('job cloning sets the time range'); await ml.jobWizardCommon.clickUseFullDataButton( 'Feb 7, 2016 @ 00:00:00.000', 'Feb 11, 2016 @ 23:59:54.000' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts new file mode 100644 index 0000000000000..73a11294b97b1 --- /dev/null +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts @@ -0,0 +1,151 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + const jobId = `fq_single_1_${Date.now()}`; + const aggAndFieldIdentifier = 'Mean(responsetime)'; + const bucketSpan = '30m'; + + function getExpectedRow(expectedJobId: string) { + return { + id: expectedJobId, + description: '', + jobGroups: [], + recordCount: '0', + memoryStatus: 'ok', + jobState: 'closed', + datafeedState: 'stopped', + latestTimestamp: '', + }; + } + + function getExpectedCounts(expectedJobId: string) { + return { + job_id: expectedJobId, + processed_record_count: '0', + processed_field_count: '0', + input_bytes: '0.0 B', + input_field_count: '0', + invalid_date_count: '0', + missing_field_count: '0', + out_of_order_timestamp_count: '0', + empty_bucket_count: '0', + sparse_bucket_count: '0', + bucket_count: '0', + }; + } + + function getExpectedModelSizeStats(expectedJobId: string) { + return { + job_id: expectedJobId, + result_type: 'model_size_stats', + total_by_field_count: '0', + total_over_field_count: '0', + total_partition_field_count: '0', + bucket_allocation_failures_count: '0', + memory_status: 'ok', + }; + } + + describe('single metric without datafeed start', function () { + this.tags(['mlqa']); + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('job creation loads the single metric wizard for the source data', async () => { + await ml.testExecution.logTestStep('job creation loads the job management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + + await ml.testExecution.logTestStep('job creation loads the new job source selection page'); + await ml.jobManagement.navigateToNewJobSourceSelection(); + + await ml.testExecution.logTestStep('job creation loads the job type selection page'); + await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob('ft_farequote'); + + await ml.testExecution.logTestStep('job creation loads the single metric job wizard page'); + await ml.jobTypeSelection.selectSingleMetricJob(); + }); + + it('job creation navigates through the single metric wizard and sets all needed fields', async () => { + await ml.testExecution.logTestStep('job creation displays the time range step'); + await ml.jobWizardCommon.assertTimeRangeSectionExists(); + + await ml.testExecution.logTestStep('job creation sets the time range'); + await ml.jobWizardCommon.clickUseFullDataButton( + 'Feb 7, 2016 @ 00:00:00.000', + 'Feb 11, 2016 @ 23:59:54.000' + ); + + await ml.testExecution.logTestStep('job creation displays the event rate chart'); + await ml.jobWizardCommon.assertEventRateChartExists(); + await ml.jobWizardCommon.assertEventRateChartHasData(); + + await ml.testExecution.logTestStep('job creation displays the pick fields step'); + await ml.jobWizardCommon.advanceToPickFieldsSection(); + + await ml.testExecution.logTestStep('job creation selects field and aggregation'); + await ml.jobWizardCommon.assertAggAndFieldInputExists(); + await ml.jobWizardCommon.selectAggAndField(aggAndFieldIdentifier, true); + await ml.jobWizardCommon.assertAnomalyChartExists('LINE'); + + await ml.testExecution.logTestStep('job creation inputs the bucket span'); + await ml.jobWizardCommon.assertBucketSpanInputExists(); + await ml.jobWizardCommon.setBucketSpan(bucketSpan); + + await ml.testExecution.logTestStep('job creation displays the job details step'); + await ml.jobWizardCommon.advanceToJobDetailsSection(); + + await ml.testExecution.logTestStep('job creation inputs the job id'); + await ml.jobWizardCommon.assertJobIdInputExists(); + await ml.jobWizardCommon.setJobId(jobId); + + await ml.testExecution.logTestStep('job creation displays the validation step'); + await ml.jobWizardCommon.advanceToValidationSection(); + + await ml.testExecution.logTestStep('job creation displays the summary step'); + await ml.jobWizardCommon.advanceToSummarySection(); + }); + + it('job creation runs the job and displays it correctly in the job list', async () => { + await ml.testExecution.logTestStep('job creation creates the job and finishes processing'); + + await ml.jobWizardCommon.assertStartDatafeedSwitchExists(); + await ml.jobWizardCommon.toggleStartDatafeedSwitch(false); + + await ml.jobWizardCommon.assertCreateJobButtonExists(); + await ml.jobWizardCommon.createJobWithoutDatafeedStart(); + + await ml.jobTable.waitForJobsToLoad(); + await ml.jobTable.filterWithSearchString(jobId, 1); + + await ml.testExecution.logTestStep( + 'job creation displays details for the created job in the job list' + ); + await ml.jobTable.assertJobRowFields(jobId, getExpectedRow(jobId)); + + await ml.jobTable.assertJobRowDetailsCounts( + jobId, + getExpectedCounts(jobId), + getExpectedModelSizeStats(jobId) + ); + }); + }); +} diff --git a/x-pack/test/functional/services/ml/job_wizard_common.ts b/x-pack/test/functional/services/ml/job_wizard_common.ts index 97253c5f45303..4ccd08c75397b 100644 --- a/x-pack/test/functional/services/ml/job_wizard_common.ts +++ b/x-pack/test/functional/services/ml/job_wizard_common.ts @@ -335,6 +335,37 @@ export function MachineLearningJobWizardCommonProvider( } }, + async assertStartDatafeedSwitchExists() { + const subj = 'mlJobWizardStartDatafeedCheckbox'; + await testSubjects.existOrFail(subj, { allowHidden: true }); + }, + + async getStartDatafeedSwitchCheckedState(): Promise { + const subj = 'mlJobWizardStartDatafeedCheckbox'; + const isSelected = await testSubjects.getAttribute(subj, 'aria-checked'); + return isSelected === 'true'; + }, + + async assertStartDatafeedSwitchCheckedState(expectedValue: boolean) { + const actualCheckedState = await this.getStartDatafeedSwitchCheckedState(); + expect(actualCheckedState).to.eql( + expectedValue, + `Expected start datafeed switch to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + actualCheckedState ? 'enabled' : 'disabled' + }')` + ); + }, + + async toggleStartDatafeedSwitch(toggle: boolean) { + const subj = 'mlJobWizardStartDatafeedCheckbox'; + if ((await this.getStartDatafeedSwitchCheckedState()) !== toggle) { + await retry.tryForTime(5 * 1000, async () => { + await testSubjects.clickWhenNotDisabled(subj); + await this.assertStartDatafeedSwitchCheckedState(toggle); + }); + } + }, + async assertModelMemoryLimitInputExists( sectionOptions: SectionOptions = { withAdvancedSection: true } ) { @@ -510,5 +541,10 @@ export function MachineLearningJobWizardCommonProvider( await testSubjects.clickWhenNotDisabled('mlJobWizardButtonCreateJob'); await testSubjects.existOrFail('mlJobWizardButtonRunInRealTime', { timeout: 2 * 60 * 1000 }); }, + + async createJobWithoutDatafeedStart() { + await testSubjects.clickWhenNotDisabled('mlJobWizardButtonCreateJob'); + await testSubjects.existOrFail('mlPageJobManagement'); + }, }; }