From c1387974f2b739a96007fb4b7b52b8e702c8ec65 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 17 Feb 2020 10:47:32 +0000 Subject: [PATCH] [ML] Categorization wizard functional tests (#57600) (#57700) * [ML] Categorization wizard functional tests * changes based on review * some idiot left his own name in the code Co-authored-by: Elastic Machine --- .../detector_cards.tsx | 2 + .../examples_valid_callout.tsx | 6 +- .../categorization_view/field_examples.tsx | 8 +- .../anomaly_detection/categorization_job.ts | 395 ++++++++++++++++++ .../anomaly_detection/index.ts | 1 + .../services/machine_learning/index.ts | 1 + .../machine_learning/job_type_selection.ts | 9 + .../job_wizard_categorization.ts | 63 +++ .../machine_learning/job_wizard_common.ts | 10 + x-pack/test/functional/services/ml.ts | 3 + 10 files changed, 496 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts create mode 100644 x-pack/test/functional/services/machine_learning/job_wizard_categorization.ts diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_detector/detector_cards.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_detector/detector_cards.tsx index 68d5fc24a96e3..991e1d5c49b7a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_detector/detector_cards.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_detector/detector_cards.tsx @@ -17,6 +17,7 @@ interface CardProps { export const CountCard: FC = ({ onClick, isSelected }) => ( = ({ onClick, isSelected }) => ( export const RareCard: FC = ({ onClick, isSelected }) => ( = ({ } return ( - + {validationChecks.map((v, i) => (
{v.message}
))} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx index 51cea179a6c0d..d3f1f0e58698b 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx @@ -57,7 +57,13 @@ export const FieldExamples: FC = ({ fieldExamples }) => { txt.push(buffer); return { example: txt }; }); - return ; + return ( + + ); }; const Token: FC = ({ children }) => ( diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts new file mode 100644 index 0000000000000..6ea9e694d2955 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts @@ -0,0 +1,395 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../legacy/plugins/ml/common/constants/new_job'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + const jobId = `categorization_${Date.now()}`; + const jobIdClone = `${jobId}_clone`; + const jobDescription = + 'Create categorization job based on the categorization_functional_test dataset with a count rare'; + const jobGroups = ['automated', 'categorization']; + const jobGroupsClone = [...jobGroups, 'clone']; + const detectorTypeIdentifier = 'Rare'; + const categorizationFieldIdentifier = 'field1'; + const categorizationExampleCount = 5; + const bucketSpan = '15m'; + const memoryLimit = '15mb'; + + function getExpectedRow(expectedJobId: string, expectedJobGroups: string[]) { + return { + id: expectedJobId, + description: jobDescription, + jobGroups: [...new Set(expectedJobGroups)].sort(), + recordCount: '1,501', + memoryStatus: 'ok', + jobState: 'closed', + datafeedState: 'stopped', + latestTimestamp: '2019-11-21 06:01:13', + }; + } + + function getExpectedCounts(expectedJobId: string) { + return { + job_id: expectedJobId, + processed_record_count: '1,501', + processed_field_count: '1,501', + input_bytes: '335.4 KB', + input_field_count: '1,501', + invalid_date_count: '0', + missing_field_count: '0', + out_of_order_timestamp_count: '0', + empty_bucket_count: '21,428', + sparse_bucket_count: '0', + bucket_count: '22,059', + earliest_record_timestamp: '2019-04-05 11:25:35', + latest_record_timestamp: '2019-11-21 06:01:13', + input_record_count: '1,501', + latest_bucket_timestamp: '2019-11-21 06:00:00', + latest_empty_bucket_timestamp: '2019-11-21 05:45:00', + }; + } + + function getExpectedModelSizeStats(expectedJobId: string) { + return { + job_id: expectedJobId, + result_type: 'model_size_stats', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '15.0 MB', + total_by_field_count: '30', + total_over_field_count: '0', + total_partition_field_count: '2', + bucket_allocation_failures_count: '0', + memory_status: 'ok', + timestamp: '2019-11-21 05:45:00', + }; + } + + describe('categorization', function() { + this.tags(['smoke', 'mlqa']); + before(async () => { + await esArchiver.load('ml/categorization'); + await ml.api.createCalendar('wizard-test-calendar'); + }); + + after(async () => { + await esArchiver.unload('ml/categorization'); + await ml.api.cleanMlIndices(); + }); + + it('job creation loads the job management page', async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + }); + + it('job creation loads the new job source selection page', async () => { + await ml.jobManagement.navigateToNewJobSourceSelection(); + }); + + it('job creation loads the job type selection page', async () => { + await ml.jobSourceSelection.selectSourceForAnomalyDetectionJob( + 'categorization_functional_test' + ); + }); + + it('job creation loads the categorization job wizard page', async () => { + await ml.jobTypeSelection.selectCategorizationJob(); + }); + + it('job creation displays the time range step', async () => { + await ml.jobWizardCommon.assertTimeRangeSectionExists(); + }); + + it('job creation sets the timerange', async () => { + await ml.jobWizardCommon.clickUseFullDataButton( + 'Apr 5, 2019 @ 11:25:35.770', + 'Nov 21, 2019 @ 06:01:13.914' + ); + }); + + it('job creation displays the event rate chart', async () => { + await ml.jobWizardCommon.assertEventRateChartExists(); + await ml.jobWizardCommon.assertEventRateChartHasData(); + }); + + it('job creation displays the pick fields step', async () => { + await ml.jobWizardCommon.advanceToPickFieldsSection(); + }); + + it(`job creation selects ${detectorTypeIdentifier} detector type`, async () => { + await ml.jobWizardCategorization.assertCategorizationDetectorTypeSelectionExists(); + await ml.jobWizardCategorization.selectCategorizationDetectorType(detectorTypeIdentifier); + }); + + it(`job creation selects the categorization field`, async () => { + await ml.jobWizardCategorization.assertCategorizationFieldInputExists(); + await ml.jobWizardCategorization.selectCategorizationField(categorizationFieldIdentifier); + await ml.jobWizardCategorization.assertCategorizationExamplesCallout( + CATEGORY_EXAMPLES_VALIDATION_STATUS.VALID + ); + await ml.jobWizardCategorization.assertCategorizationExamplesTable( + categorizationExampleCount + ); + }); + + it('job creation inputs the bucket span', async () => { + await ml.jobWizardCommon.assertBucketSpanInputExists(); + await ml.jobWizardCommon.setBucketSpan(bucketSpan); + }); + + it('job creation displays the job details step', async () => { + await ml.jobWizardCommon.advanceToJobDetailsSection(); + }); + + it('job creation inputs the job id', async () => { + await ml.jobWizardCommon.assertJobIdInputExists(); + await ml.jobWizardCommon.setJobId(jobId); + }); + + it('job creation inputs the job description', async () => { + await ml.jobWizardCommon.assertJobDescriptionInputExists(); + await ml.jobWizardCommon.setJobDescription(jobDescription); + }); + + it('job creation inputs job groups', async () => { + await ml.jobWizardCommon.assertJobGroupInputExists(); + for (const jobGroup of jobGroups) { + await ml.jobWizardCommon.addJobGroup(jobGroup); + } + await ml.jobWizardCommon.assertJobGroupSelection(jobGroups); + }); + + it('job creation opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job creation adds a new custom url', async () => { + await ml.jobWizardCommon.addCustomUrl({ label: 'check-kibana-dashboard' }); + }); + + it('job creation assigns calendars', async () => { + await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + }); + + it('job creation opens the advanced section', async () => { + await ml.jobWizardCommon.ensureAdvancedSectionOpen(); + }); + + it('job creation displays the model plot switch', async () => { + await ml.jobWizardCommon.assertModelPlotSwitchExists(); + await ml.jobWizardCommon.assertModelPlotSwitchEnabled(false); + await ml.jobWizardCommon.assertModelPlotSwitchCheckedState(false); + }); + + it('job creation enables the dedicated index switch', async () => { + await ml.jobWizardCommon.assertDedicatedIndexSwitchExists(); + await ml.jobWizardCommon.activateDedicatedIndexSwitch(); + }); + + it('job creation inputs the model memory limit', async () => { + await ml.jobWizardCommon.assertModelMemoryLimitInputExists(); + await ml.jobWizardCommon.setModelMemoryLimit(memoryLimit); + }); + + it('job creation displays the validation step', async () => { + await ml.jobWizardCommon.advanceToValidationSection(); + }); + + it('job creation displays the summary step', async () => { + await ml.jobWizardCommon.advanceToSummarySection(); + }); + + it('job creation creates the job and finishes processing', async () => { + await ml.jobWizardCommon.assertCreateJobButtonExists(); + await ml.jobWizardCommon.createJobAndWaitForCompletion(); + }); + + it('job creation displays the created job in the job list', async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + + await ml.jobTable.waitForJobsToLoad(); + await ml.jobTable.filterWithSearchString(jobId); + const rows = await ml.jobTable.parseJobTable(); + expect(rows.filter(row => row.id === jobId)).to.have.length(1); + }); + + it('job creation displays details for the created job in the job list', async () => { + await ml.jobTable.assertJobRowFields(jobId, getExpectedRow(jobId, jobGroups)); + + await ml.jobTable.assertJobRowDetailsCounts( + jobId, + getExpectedCounts(jobId), + getExpectedModelSizeStats(jobId) + ); + }); + + it('job creation has detector results', async () => { + await ml.api.assertDetectorResultsExist(jobId, 0); + }); + + it('job cloning clicks the clone action and loads the single metric wizard', async () => { + await ml.jobTable.clickCloneJobAction(jobId); + await ml.jobTypeSelection.assertCategorizationJobWizardOpen(); + }); + + it('job cloning displays the time range step', async () => { + await ml.jobWizardCommon.assertTimeRangeSectionExists(); + }); + + it('job cloning sets the timerange', async () => { + await ml.jobWizardCommon.clickUseFullDataButton( + 'Apr 5, 2019 @ 11:25:35.770', + 'Nov 21, 2019 @ 06:01:13.914' + ); + }); + + it('job cloning displays the event rate chart', async () => { + await ml.jobWizardCommon.assertEventRateChartExists(); + await ml.jobWizardCommon.assertEventRateChartHasData(); + }); + + it('job cloning displays the pick fields step', async () => { + await ml.jobWizardCommon.advanceToPickFieldsSection(); + }); + + it('job cloning pre-fills field and aggregation', async () => { + await ml.jobWizardCategorization.assertCategorizationDetectorTypeSelectionExists(); + }); + + it('job cloning pre-fills the bucket span', async () => { + await ml.jobWizardCommon.assertBucketSpanInputExists(); + await ml.jobWizardCommon.assertBucketSpanValue(bucketSpan); + }); + + it('job cloning displays the job details step', async () => { + await ml.jobWizardCommon.advanceToJobDetailsSection(); + }); + + it('job cloning does not pre-fill the job id', async () => { + await ml.jobWizardCommon.assertJobIdInputExists(); + await ml.jobWizardCommon.assertJobIdValue(''); + }); + + it('job cloning inputs the clone job id', async () => { + await ml.jobWizardCommon.setJobId(jobIdClone); + }); + + it('job cloning pre-fills the job description', async () => { + await ml.jobWizardCommon.assertJobDescriptionInputExists(); + await ml.jobWizardCommon.assertJobDescriptionValue(jobDescription); + }); + + it('job cloning pre-fills job groups', async () => { + await ml.jobWizardCommon.assertJobGroupInputExists(); + await ml.jobWizardCommon.assertJobGroupSelection(jobGroups); + }); + + it('job cloning inputs the clone job group', async () => { + await ml.jobWizardCommon.assertJobGroupInputExists(); + await ml.jobWizardCommon.addJobGroup('clone'); + await ml.jobWizardCommon.assertJobGroupSelection(jobGroupsClone); + }); + + it('job cloning opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job cloning persists custom urls', async () => { + await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + }); + + it('job cloning persists assigned calendars', async () => { + await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + }); + + it('job cloning opens the advanced section', async () => { + await ml.jobWizardCommon.ensureAdvancedSectionOpen(); + }); + + it('job cloning pre-fills the model plot switch', async () => { + await ml.jobWizardCommon.assertModelPlotSwitchExists(); + await ml.jobWizardCommon.assertModelPlotSwitchEnabled(false); + await ml.jobWizardCommon.assertModelPlotSwitchCheckedState(false); + }); + + it('job cloning pre-fills the dedicated index switch', async () => { + await ml.jobWizardCommon.assertDedicatedIndexSwitchExists(); + await ml.jobWizardCommon.assertDedicatedIndexSwitchCheckedState(true); + }); + + it('job cloning pre-fills the model memory limit', async () => { + await ml.jobWizardCommon.assertModelMemoryLimitInputExists(); + await ml.jobWizardCommon.assertModelMemoryLimitValue(memoryLimit); + }); + + it('job cloning displays the validation step', async () => { + await ml.jobWizardCommon.advanceToValidationSection(); + }); + + it('job cloning displays the summary step', async () => { + await ml.jobWizardCommon.advanceToSummarySection(); + }); + + it('job cloning creates the job and finishes processing', async () => { + await ml.jobWizardCommon.assertCreateJobButtonExists(); + await ml.jobWizardCommon.createJobAndWaitForCompletion(); + }); + + it('job cloning displays the created job in the job list', async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + + await ml.jobTable.waitForJobsToLoad(); + await ml.jobTable.filterWithSearchString(jobIdClone); + const rows = await ml.jobTable.parseJobTable(); + expect(rows.filter(row => row.id === jobIdClone)).to.have.length(1); + }); + + it('job cloning displays details for the created job in the job list', async () => { + await ml.jobTable.assertJobRowFields(jobIdClone, getExpectedRow(jobIdClone, jobGroupsClone)); + + await ml.jobTable.assertJobRowDetailsCounts( + jobIdClone, + getExpectedCounts(jobIdClone), + getExpectedModelSizeStats(jobIdClone) + ); + }); + + it('job cloning has detector results', async () => { + await ml.api.assertDetectorResultsExist(jobId, 0); + }); + + it('job deletion has results for the job before deletion', async () => { + await ml.api.assertJobResultsExist(jobIdClone); + }); + + it('job deletion triggers the delete action', async () => { + await ml.jobTable.clickDeleteJobAction(jobIdClone); + }); + + it('job deletion confirms the delete modal', async () => { + await ml.jobTable.confirmDeleteJobModal(); + }); + + it('job deletion does not display the deleted job in the job list any more', async () => { + await ml.jobTable.waitForJobsToLoad(); + await ml.jobTable.filterWithSearchString(jobIdClone); + const rows = await ml.jobTable.parseJobTable(); + expect(rows.filter(row => row.id === jobIdClone)).to.have.length(0); + }); + + it('job deletion does not have results for the deleted job any more', async () => { + await ml.api.assertNoJobResultsExist(jobIdClone); + }); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts index a52e3d3aca2c0..28e8b221cff4e 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts @@ -16,5 +16,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./advanced_job')); loadTestFile(require.resolve('./single_metric_viewer')); loadTestFile(require.resolve('./anomaly_explorer')); + loadTestFile(require.resolve('./categorization_job')); }); } diff --git a/x-pack/test/functional/services/machine_learning/index.ts b/x-pack/test/functional/services/machine_learning/index.ts index b01f127519670..4cecd27631e18 100644 --- a/x-pack/test/functional/services/machine_learning/index.ts +++ b/x-pack/test/functional/services/machine_learning/index.ts @@ -21,6 +21,7 @@ export { MachineLearningJobTableProvider } from './job_table'; export { MachineLearningJobTypeSelectionProvider } from './job_type_selection'; export { MachineLearningJobWizardAdvancedProvider } from './job_wizard_advanced'; export { MachineLearningJobWizardCommonProvider } from './job_wizard_common'; +export { MachineLearningJobWizardCategorizationProvider } from './job_wizard_categorization'; export { MachineLearningJobWizardMultiMetricProvider } from './job_wizard_multi_metric'; export { MachineLearningJobWizardPopulationProvider } from './job_wizard_population'; export { MachineLearningNavigationProvider } from './navigation'; diff --git a/x-pack/test/functional/services/machine_learning/job_type_selection.ts b/x-pack/test/functional/services/machine_learning/job_type_selection.ts index 6686b5b28f200..be66c53326a23 100644 --- a/x-pack/test/functional/services/machine_learning/job_type_selection.ts +++ b/x-pack/test/functional/services/machine_learning/job_type_selection.ts @@ -45,5 +45,14 @@ export function MachineLearningJobTypeSelectionProvider({ getService }: FtrProvi async assertAdvancedJobWizardOpen() { await testSubjects.existOrFail('mlPageJobWizard advanced'); }, + + async selectCategorizationJob() { + await testSubjects.clickWhenNotDisabled('mlJobTypeLinkCategorizationJob'); + await this.assertCategorizationJobWizardOpen(); + }, + + async assertCategorizationJobWizardOpen() { + await testSubjects.existOrFail('mlPageJobWizard categorization'); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_categorization.ts b/x-pack/test/functional/services/machine_learning/job_wizard_categorization.ts new file mode 100644 index 0000000000000..cb590c7022965 --- /dev/null +++ b/x-pack/test/functional/services/machine_learning/job_wizard_categorization.ts @@ -0,0 +1,63 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../legacy/plugins/ml/common/constants/new_job'; + +export function MachineLearningJobWizardCategorizationProvider({ getService }: FtrProviderContext) { + const comboBox = getService('comboBox'); + const testSubjects = getService('testSubjects'); + + return { + async assertCategorizationDetectorTypeSelectionExists() { + await testSubjects.existOrFail('~mlJobWizardCategorizationDetectorCountCard'); + await testSubjects.existOrFail('~mlJobWizardCategorizationDetectorRareCard'); + }, + + async selectCategorizationDetectorType(identifier: string) { + const id = `~mlJobWizardCategorizationDetector${identifier}Card`; + await testSubjects.existOrFail(id); + await testSubjects.clickWhenNotDisabled(id); + await testSubjects.existOrFail(`mlJobWizardCategorizationDetector${identifier}Card selected`); + }, + + async assertCategorizationFieldInputExists() { + await testSubjects.existOrFail('mlCategorizationFieldNameSelect > comboBoxInput'); + }, + + async selectCategorizationField(identifier: string) { + await comboBox.set('mlCategorizationFieldNameSelect > comboBoxInput', identifier); + + await this.assertCategorizationFieldSelection([identifier]); + }, + + async assertCategorizationFieldSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCategorizationFieldNameSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected categorization field selection to be '${expectedIdentifier}' (got ${comboBoxSelectedOptions}')` + ); + }, + + async assertCategorizationExamplesCallout(status: CATEGORY_EXAMPLES_VALIDATION_STATUS) { + await testSubjects.existOrFail(`mlJobWizardCategorizationExamplesCallout ${status}`); + }, + + async assertCategorizationExamplesTable(exampleCount: number) { + const table = await testSubjects.find('mlJobWizardCategorizationExamplesTable'); + const body = await table.findAllByTagName('tbody'); + expect(body.length).to.eql(1, `Expected categorization field examples table to have a body`); + const rows = await body[0].findAllByTagName('tr'); + expect(rows.length).to.eql( + exampleCount, + `Expected categorization field examples table to have '${exampleCount}' rows (got ${rows.length}')` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index c2f408276d9e4..38e6694669c1a 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -224,6 +224,16 @@ export function MachineLearningJobWizardCommonProvider( expect(actualCheckedState).to.eql(expectedValue); }, + async assertModelPlotSwitchEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlJobWizardSwitchModelPlot'); + expect(isEnabled).to.eql( + expectedValue, + `Expected model plot switch to be '${expectedValue ? 'enabled' : 'disabled'}' (got ${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async assertDedicatedIndexSwitchExists( sectionOptions: SectionOptions = { withAdvancedSection: true } ) { diff --git a/x-pack/test/functional/services/ml.ts b/x-pack/test/functional/services/ml.ts index 5957a8a2eeb0a..18574c62b84d9 100644 --- a/x-pack/test/functional/services/ml.ts +++ b/x-pack/test/functional/services/ml.ts @@ -23,6 +23,7 @@ import { MachineLearningJobTableProvider, MachineLearningJobTypeSelectionProvider, MachineLearningJobWizardAdvancedProvider, + MachineLearningJobWizardCategorizationProvider, MachineLearningJobWizardCommonProvider, MachineLearningJobWizardMultiMetricProvider, MachineLearningJobWizardPopulationProvider, @@ -49,6 +50,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const jobTable = MachineLearningJobTableProvider(context); const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context); const jobWizardAdvanced = MachineLearningJobWizardAdvancedProvider(context, common); + const jobWizardCategorization = MachineLearningJobWizardCategorizationProvider(context); const jobWizardCommon = MachineLearningJobWizardCommonProvider(context, common, customUrls); const jobWizardMultiMetric = MachineLearningJobWizardMultiMetricProvider(context); const jobWizardPopulation = MachineLearningJobWizardPopulationProvider(context); @@ -73,6 +75,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { jobTable, jobTypeSelection, jobWizardAdvanced, + jobWizardCategorization, jobWizardCommon, jobWizardMultiMetric, jobWizardPopulation,