[ML] Adding option to create AD jobs without starting the datafeed (#77484)

* [ML] Adding option to create AD jobs without starting the datafeed

* changing translation id

* i just need some space

* adding missed spelling change

* disabling switch when running

* improving logic

* further test improvements

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
James Gowdy 2020-09-17 09:00:49 +01:00 committed by GitHub
parent 06e8c54751
commit ae55391035
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 273 additions and 18 deletions

View file

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

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 { StartDatafeedSwitch } from './start_datafeed_switch';

View file

@ -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<Props> = ({
startDatafeed,
setStartDatafeed,
disabled = false,
}) => {
return (
<>
<EuiSpacer />
<EuiFormRow
helpText={i18n.translate(
'xpack.ml.newJob.wizard.summaryStep.startDatafeedCheckboxHelpText',
{
defaultMessage: 'If unselected, job can be started later from the jobs list.',
}
)}
>
<EuiSwitch
data-test-subj="mlJobWizardStartDatafeedCheckbox"
label={i18n.translate('xpack.ml.newJob.wizard.summaryStep.startDatafeedCheckbox', {
defaultMessage: 'Start immediately',
})}
checked={startDatafeed}
onChange={(e) => setStartDatafeed(e.target.checked)}
disabled={disabled}
/>
</EuiFormRow>
</>
);
};

View file

@ -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<StepProps> = ({ setCurrentStep, isCurrentStep }) =>
const [creatingJob, setCreatingJob] = useState(false);
const [isValid, setIsValid] = useState(jobValidator.validationSummary.basic);
const [jobRunner, setJobRunner] = useState<JobRunner | null>(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<StepProps> = ({ 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<StepProps> = ({ 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<StepProps> = ({ setCurrentStep, isCurrentStep }) =>
<EuiSpacer size="m" />
<JobDetails />
{isAdvanced === false && (
<StartDatafeedSwitch
startDatafeed={startDatafeed}
setStartDatafeed={setStartDatafeed}
disabled={creatingJob}
/>
)}
{isAdvanced && (
<Fragment>
<EuiHorizontalRule />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -335,6 +335,37 @@ export function MachineLearningJobWizardCommonProvider(
}
},
async assertStartDatafeedSwitchExists() {
const subj = 'mlJobWizardStartDatafeedCheckbox';
await testSubjects.existOrFail(subj, { allowHidden: true });
},
async getStartDatafeedSwitchCheckedState(): Promise<boolean> {
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');
},
};
}