Skip to content

Commit

Permalink
[ML] Adding option to create AD jobs without starting the datafeed
Browse files Browse the repository at this point in the history
  • Loading branch information
jgowdyelastic committed Sep 15, 2020
1 parent 37b9c81 commit 3f28920
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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;
}

export const StartDatafeedSwitch: FC<Props> = ({ startDatafeed, setStartDatafeed }) => {
return (
<>
<EuiSpacer />
<EuiFormRow
helpText={i18n.translate('xpack.ml.newJob.wizard.summaryStep.startCheckboxHelpText', {
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)}
/>
</EuiFormRow>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -60,13 +62,15 @@ export const SummaryStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) =>

async function start() {
if (isAdvanced) {
await startAdvanced();
await createAdvancedJob();
} else if (startDatafeed === true) {
await createAndStartJob();
} else {
await startInline();
await createAdvancedJob(false);
}
}

async function startInline() {
async function createAndStartJob() {
setCreatingJob(true);
try {
const jr = await jobCreator.createAndStartJob();
Expand All @@ -76,12 +80,12 @@ export const SummaryStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) =>
}
}

async function startAdvanced() {
async function createAdvancedJob(showStartModal: boolean = true) {
setCreatingJob(true);
try {
await jobCreator.createJob();
await jobCreator.createDatafeed();
advancedStartDatafeed(jobCreator, navigateToPath);
advancedStartDatafeed(showStartModal ? jobCreator : null, navigateToPath);
} catch (error) {
handleJobCreationError(error);
}
Expand Down Expand Up @@ -131,6 +135,13 @@ export const SummaryStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) =>
<EuiSpacer size="m" />
<JobDetails />

{isAdvanced === false && (
<StartDatafeedSwitch
startDatafeed={startDatafeed}
setStartDatafeed={setStartDatafeed}
/>
)}

{isAdvanced && (
<Fragment>
<EuiHorizontalRule />
Expand Down
1 change: 1 addition & 0 deletions x-pack/test/functional/apps/ml/anomaly_detection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* 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',
};
}

const calendarId = `wizard-test-calendar_${Date.now()}`;

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.api.createCalendar(calendarId);
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 timerange');
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.createJobAndDoNotWaitForCompletion();

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)
);
});
});
}
35 changes: 35 additions & 0 deletions x-pack/test/functional/services/ml/job_wizard_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
) {
Expand Down Expand Up @@ -510,5 +541,9 @@ export function MachineLearningJobWizardCommonProvider(
await testSubjects.clickWhenNotDisabled('mlJobWizardButtonCreateJob');
await testSubjects.existOrFail('mlJobWizardButtonRunInRealTime', { timeout: 2 * 60 * 1000 });
},

async createJobAndDoNotWaitForCompletion() {
await testSubjects.clickWhenNotDisabled('mlJobWizardButtonCreateJob');
},
};
}

0 comments on commit 3f28920

Please sign in to comment.