From 9ed88f1927e229bf8453d496ca6259633536b8de Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 31 Aug 2021 09:08:57 +0100 Subject: [PATCH 01/10] [ML] Job import export functional tests --- .../export_jobs_flyout/export_jobs_flyout.tsx | 7 +- .../cannot_import_jobs_callout.tsx | 1 + .../import_jobs_flyout/import_jobs_flyout.tsx | 18 +- .../ml/stack_management_jobs/export_jobs.ts | 25 ++ .../anomaly_detection_jobs.json | 213 ++++++++++++++++++ .../ml/stack_management_jobs/import_jobs.ts | 69 ++++++ .../apps/ml/stack_management_jobs/index.ts | 1 + .../services/ml/stack_management_jobs.ts | 68 +++++- 8 files changed, 396 insertions(+), 6 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx index bd4b805baa186..0a23dc4a2d9c0 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -211,7 +211,12 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { {showFlyout === true && isDisabled === false && ( <> - setShowFlyout(false)} hideCloseButton size="s"> + setShowFlyout(false)} + hideCloseButton + size="s" + data-test-subj="mlJobMgmtExportJobsFlyout" + >

diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx index 732be345a1ee4..565ded9c6f6c3 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx @@ -30,6 +30,7 @@ export const CannotImportJobsCallout: FC = ({ jobs, autoExpand = false }) values: { num: jobs.length }, })} color="warning" + data-test-subj="mlJobMgmtImportJobsCannotBeImportedCallout" > {autoExpand ? ( diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx index 68db42cdbf0eb..d116e78cdc70d 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -341,7 +341,12 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { {showFlyout === true && isDisabled === false && ( - +

@@ -373,7 +378,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { {showFileReadError ? : null} {totalJobsRead > 0 && jobType !== null && ( - <> +
{jobType === 'anomaly-detector' && ( = ({ isDisabled }) => {
))} - + )} @@ -484,7 +489,12 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { - + { + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); + await ml.navigation.navigateToStackManagement(); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json new file mode 100644 index 0000000000000..a3a3e63b784ea --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json @@ -0,0 +1,213 @@ +[ + { + "job": { + "job_id": "test1", + "description": "", + "analysis_config": { + "bucket_span": "15m", + "summary_count_field_name": "doc_count", + "detectors": [ + { + "detector_description": "mean(responsetime)", + "function": "mean", + "field_name": "responsetime", + "detector_index": 0 + } + ], + "influencers": [] + }, + "analysis_limits": { + "model_memory_limit": "11mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": true, + "annotations_enabled": true + }, + "model_snapshot_retention_days": 10, + "daily_model_snapshot_retention_after_days": 1, + "results_index_name": "shared", + "allow_lazy_open": false + }, + "datafeed": { + "datafeed_id": "datafeed-test1", + "job_id": "test1", + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ] + } + }, + "indices": [ + "ft_farequote" + ], + "aggregations": { + "buckets": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "90000ms" + }, + "aggregations": { + "responsetime": { + "avg": { + "field": "responsetime" + } + }, + "@timestamp": { + "max": { + "field": "@timestamp" + } + } + } + } + }, + "scroll_size": 1000, + "delayed_data_check_config": { + "enabled": true + } + } + }, + { + "job": { + "job_id": "test2", + "groups": [ + "newgroup" + ], + "description": "", + "analysis_config": { + "bucket_span": "15m", + "summary_count_field_name": "doc_count", + "detectors": [ + { + "detector_description": "mean(responsetime)", + "function": "mean", + "field_name": "responsetime", + "detector_index": 0 + } + ], + "influencers": [] + }, + "analysis_limits": { + "model_memory_limit": "11mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": true, + "annotations_enabled": true + }, + "model_snapshot_retention_days": 10, + "daily_model_snapshot_retention_after_days": 1, + "results_index_name": "shared", + "allow_lazy_open": false + }, + "datafeed": { + "datafeed_id": "datafeed-test2", + "job_id": "test2", + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ] + } + }, + "indices": [ + "missing" + ], + "aggregations": { + "buckets": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "90000ms" + }, + "aggregations": { + "responsetime": { + "avg": { + "field": "responsetime" + } + }, + "@timestamp": { + "max": { + "field": "@timestamp" + } + } + } + } + }, + "scroll_size": 1000, + "delayed_data_check_config": { + "enabled": true + } + } + }, + { + "job": { + "job_id": "test3", + "custom_settings": {}, + "description": "", + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "mean(responsetime) partitionfield=airline", + "function": "mean", + "field_name": "responsetime", + "partition_field_name": "airline", + "detector_index": 0 + } + ], + "influencers": [ + "airline" + ] + }, + "analysis_limits": { + "model_memory_limit": "11mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": false, + "annotations_enabled": false + }, + "model_snapshot_retention_days": 10, + "daily_model_snapshot_retention_after_days": 1, + "results_index_name": "shared", + "allow_lazy_open": false + }, + "datafeed": { + "datafeed_id": "datafeed-test3", + "job_id": "test3", + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ] + } + }, + "indices": [ + "ft_farequote" + ], + "scroll_size": 1000, + "delayed_data_check_config": { + "enabled": true + } + } + } +] diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts new file mode 100644 index 0000000000000..c0bc899eca887 --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import path from 'path'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const testDataListPositive = [ + { + filePath: path.join(__dirname, 'files_to_import', 'anomaly_detection_jobs.json'), + expected: { + jobIds: ['test1', 'test3'], + skippedJobIds: ['test2'], + }, + }, + ]; + + describe.only('import jobs', function () { + this.tags(['mlqa']); + before(async () => { + await ml.api.cleanMlIndices(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); + await ml.navigation.navigateToStackManagement(); + await ml.navigation.navigateToStackManagementJobsListPage(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataListPositive) { + it('selects and reads file', async () => { + await ml.testExecution.logTestStep('selects job import'); + await ml.stackManagementJobs.openImportFlyout(); + await ml.stackManagementJobs.selectFileToImport(testData.filePath); + }); + it('has the correct importable jobs', async () => { + await ml.stackManagementJobs.assertJobIdsExist(testData.expected.jobIds); + await ml.stackManagementJobs.assertJobIdsSkipped(testData.expected.skippedJobIds); + }); + + it('imports jobs', async () => { + await ml.stackManagementJobs.importJobs(); + }); + + it('ensures jobs have been imported', async () => { + await ml.jobTable.refreshJobList(); + for (const id of testData.expected.jobIds) { + await ml.jobTable.filterWithSearchString(id); + } + for (const id of testData.expected.skippedJobIds) { + await ml.jobTable.filterWithSearchString(id, 0); + } + }); + } + }); +} diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts index f120ab0b450dc..4dc7dc6d69930 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./synchronize')); loadTestFile(require.resolve('./manage_spaces')); + loadTestFile(require.resolve('./import_jobs')); }); } diff --git a/x-pack/test/functional/services/ml/stack_management_jobs.ts b/x-pack/test/functional/services/ml/stack_management_jobs.ts index 48fb89e51ff11..52a74af4f8965 100644 --- a/x-pack/test/functional/services/ml/stack_management_jobs.ts +++ b/x-pack/test/functional/services/ml/stack_management_jobs.ts @@ -18,7 +18,7 @@ type SyncFlyoutObjectType = | 'ObjectsUnmatchedDatafeed'; export function MachineLearningStackManagementJobsProvider( - { getService }: FtrProviderContext, + { getService, getPageObjects }: FtrProviderContext, mlADJobTable: MlADJobTable, mlDFAJobTable: MlDFAJobTable ) { @@ -26,6 +26,9 @@ export function MachineLearningStackManagementJobsProvider( const retry = getService('retry'); const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); + const log = getService('log'); + + const PageObjects = getPageObjects(['common']); return { async openSyncFlyout() { @@ -194,5 +197,68 @@ export function MachineLearningStackManagementJobsProvider( } await this.assertSpaceSelectionRowSelected(spaceId, shouldSelect); }, + + async openImportFlyout() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('mlJobsImportButton', 1000); + await testSubjects.existOrFail('mlJobMgmtImportJobsFlyout'); + }); + }, + + async selectFileToImport(path: string, expectError: boolean = false) { + log.debug(`Importing file '${path}' ...`); + await PageObjects.common.setFileInputPath(path); + + if (expectError) { + await testSubjects.existOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + } else { + await testSubjects.missingOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + await testSubjects.existOrFail('mlJobMgmtImportJobsFileRead'); + } + }, + + async assertJobIdsExist(expectedJobIds: string[]) { + const subj = await testSubjects.find('mlJobMgmtImportJobsFileRead'); + const inputs = await subj.findAllByTagName('input'); + const actualJobIds = await Promise.all(inputs.map((i) => i.getAttribute('value'))); + + expect(actualJobIds.sort()).to.eql( + expectedJobIds.sort(), + `Expected job ids to be '${JSON.stringify(expectedJobIds)}' (got '${JSON.stringify( + actualJobIds + )}')` + ); + }, + + async assertJobIdsSkipped(expectedJobIds: string[]) { + const subj = await testSubjects.find('mlJobMgmtImportJobsCannotBeImportedCallout'); + const skippedJobTitles = await subj.findAllByTagName('h5'); + const actualJobIds = ( + await Promise.all(skippedJobTitles.map((i) => i.parseDomContent())) + ).map((t) => t.html()); + + expect(actualJobIds.sort()).to.eql( + expectedJobIds.sort(), + `Expected job ids to be '${JSON.stringify(expectedJobIds)}' (got '${JSON.stringify( + actualJobIds + )}')` + ); + }, + + async importJobs() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('mlJobMgmtImportImportButton', 1000); + }); + await retry.tryForTime(40000, async () => { + await testSubjects.missingOrFail('mlJobMgmtImportJobsFlyout'); + }); + }, + + async assertImportedJobIdsExist(expectedJobIds: string[]) { + await retry.tryForTime(40000, async () => { + await testSubjects.click('mlRefreshJobListButton', 1000); + await testSubjects.missingOrFail('mlJobMgmtImportJobsFlyout'); + }); + }, }; } From c9d03d98af80d5ea2168eb8a1a511fdc82fed268 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 31 Aug 2021 16:44:13 +0100 Subject: [PATCH 02/10] adding title check --- .../import_jobs_flyout/import_jobs_flyout.tsx | 24 +++++++++++-------- .../ml/stack_management_jobs/import_jobs.ts | 10 ++++++-- .../services/ml/stack_management_jobs.ts | 24 +++++++++++++++++++ 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx index d116e78cdc70d..a5793b3558a6c 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -381,19 +381,23 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => {
{jobType === 'anomaly-detector' && ( - +
+ +
)} {jobType === 'data-frame-analytics' && ( - +
+ +
)} diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts index c0bc899eca887..cc9c7ec20409c 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -8,7 +8,8 @@ import path from 'path'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; +// import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; +import { JobType } from '../../../../../plugins/ml/common/types/saved_objects'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -17,13 +18,14 @@ export default function ({ getService }: FtrProviderContext) { { filePath: path.join(__dirname, 'files_to_import', 'anomaly_detection_jobs.json'), expected: { + jobType: 'anomaly-detector' as JobType, jobIds: ['test1', 'test3'], skippedJobIds: ['test2'], }, }, ]; - describe.only('import jobs', function () { + describe('import jobs', function () { this.tags(['mlqa']); before(async () => { await ml.api.cleanMlIndices(); @@ -47,6 +49,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.stackManagementJobs.selectFileToImport(testData.filePath); }); it('has the correct importable jobs', async () => { + await ml.stackManagementJobs.assertCorrectTitle( + [...testData.expected.jobIds, ...testData.expected.skippedJobIds].length, + testData.expected.jobType + ); await ml.stackManagementJobs.assertJobIdsExist(testData.expected.jobIds); await ml.stackManagementJobs.assertJobIdsSkipped(testData.expected.skippedJobIds); }); diff --git a/x-pack/test/functional/services/ml/stack_management_jobs.ts b/x-pack/test/functional/services/ml/stack_management_jobs.ts index 52a74af4f8965..30221f07e1cb2 100644 --- a/x-pack/test/functional/services/ml/stack_management_jobs.ts +++ b/x-pack/test/functional/services/ml/stack_management_jobs.ts @@ -10,6 +10,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlADJobTable } from './job_table'; import { MlDFAJobTable } from './data_frame_analytics_table'; +import { JobType } from '../../../../plugins/ml/common/types/saved_objects'; type SyncFlyoutObjectType = | 'MissingObjects' @@ -230,6 +231,29 @@ export function MachineLearningStackManagementJobsProvider( ); }, + async assertCorrectTitle(jobCount: number, jobType: JobType) { + const subj = await testSubjects.find('mlJobMgmtImportJobsADTitle'); + const title = (await subj.parseDomContent()).html(); + + const jobTypeString = + jobType === 'anomaly-detector' ? 'anomaly detection' : 'data frame analytics'; + + const results = title.match( + /(\d) (anomaly detection|data frame analytics) jobs read from file$/ + ); + expect(results).to.not.eql(null, `Expected regex results to not be null`); + const foundCount = results![1]; + const foundJobTypeString = results![2]; + expect(foundCount).to.eql( + jobCount, + `Expected job count to be '${jobCount}' (got '${foundCount}')` + ); + expect(foundJobTypeString).to.eql( + jobTypeString, + `Expected job count to be '${jobTypeString}' (got '${foundJobTypeString}')` + ); + }, + async assertJobIdsSkipped(expectedJobIds: string[]) { const subj = await testSubjects.find('mlJobMgmtImportJobsCannotBeImportedCallout'); const skippedJobTitles = await subj.findAllByTagName('h5'); From 9b93f5b079717c0409c1d2d0961d918b103ed5a5 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 1 Sep 2021 11:56:20 +0100 Subject: [PATCH 03/10] adding dfa tests --- .../import_jobs_flyout/import_jobs_flyout.tsx | 1 + .../anomaly_detection_jobs.json | 18 +++--- .../data_frame_analytics_jobs.json | 60 +++++++++++++++++++ .../ml/stack_management_jobs/import_jobs.ts | 38 +++++++++--- .../services/ml/stack_management_jobs.ts | 12 ++-- 5 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs.json diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx index a5793b3558a6c..dfe07b1984e11 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -435,6 +435,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { value={jobId.jobId} onChange={(e) => renameJob(e.target.value, i)} isInvalid={jobId.jobIdValid === false} + data-test-subj="mlJobMgmtImportJobIdInput" /> diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json index a3a3e63b784ea..1bc51d433858e 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json @@ -1,7 +1,7 @@ [ { "job": { - "job_id": "test1", + "job_id": "ad-test1", "description": "", "analysis_config": { "bucket_span": "15m", @@ -34,8 +34,8 @@ "allow_lazy_open": false }, "datafeed": { - "datafeed_id": "datafeed-test1", - "job_id": "test1", + "datafeed_id": "datafeed-ad-test1", + "job_id": "ad-test1", "query": { "bool": { "must": [ @@ -76,7 +76,7 @@ }, { "job": { - "job_id": "test2", + "job_id": "ad-test2", "groups": [ "newgroup" ], @@ -112,8 +112,8 @@ "allow_lazy_open": false }, "datafeed": { - "datafeed_id": "datafeed-test2", - "job_id": "test2", + "datafeed_id": "datafeed-ad-test2", + "job_id": "ad-test2", "query": { "bool": { "must": [ @@ -154,7 +154,7 @@ }, { "job": { - "job_id": "test3", + "job_id": "ad-test3", "custom_settings": {}, "description": "", "analysis_config": { @@ -190,8 +190,8 @@ "allow_lazy_open": false }, "datafeed": { - "datafeed_id": "datafeed-test3", - "job_id": "test3", + "datafeed_id": "datafeed-ad-test3", + "job_id": "ad-test3", "query": { "bool": { "must": [ diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs.json new file mode 100644 index 0000000000000..cb93aa9e24c5f --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs.json @@ -0,0 +1,60 @@ +[ + { + "id": "dfa-test1", + "description": "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + "source": { + "index": [ + "ft_bank_marketing" + ], + "query": { + "match_all": {} + } + }, + "dest": { + "index": "user-dfa-test1", + "results_field": "ml" + }, + "analysis": { + "classification": { + "prediction_field_name": "user-test", + "dependent_variable": "y", + "training_percent": 20 + } + }, + "analyzed_fields": { + "includes": [], + "excludes": [] + }, + "model_memory_limit": "60mb", + "allow_lazy_start": false + }, + { + "id": "dfa-test2", + "description": "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + "source": { + "index": [ + "missing-index" + ], + "query": { + "match_all": {} + } + }, + "dest": { + "index": "user-dfa-test2", + "results_field": "ml" + }, + "analysis": { + "classification": { + "prediction_field_name": "test", + "dependent_variable": "y", + "training_percent": 20 + } + }, + "analyzed_fields": { + "includes": [], + "excludes": [] + }, + "model_memory_limit": "60mb", + "allow_lazy_start": false + } +] diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts index cc9c7ec20409c..b1cb448f00a59 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -19,8 +19,16 @@ export default function ({ getService }: FtrProviderContext) { filePath: path.join(__dirname, 'files_to_import', 'anomaly_detection_jobs.json'), expected: { jobType: 'anomaly-detector' as JobType, - jobIds: ['test1', 'test3'], - skippedJobIds: ['test2'], + jobIds: ['ad-test1', 'ad-test3'], + skippedJobIds: ['ad-test2'], + }, + }, + { + filePath: path.join(__dirname, 'files_to_import', 'data_frame_analytics_jobs.json'), + expected: { + jobType: 'data-frame-analytics' as JobType, + jobIds: ['dfa-test1'], + skippedJobIds: ['dfa-test2'], }, }, ]; @@ -30,7 +38,9 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await ml.api.cleanMlIndices(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); await ml.testResources.setKibanaTimeZoneToUTC(); await ml.securityUI.loginAsMlPowerUser(); @@ -62,12 +72,24 @@ export default function ({ getService }: FtrProviderContext) { }); it('ensures jobs have been imported', async () => { - await ml.jobTable.refreshJobList(); - for (const id of testData.expected.jobIds) { - await ml.jobTable.filterWithSearchString(id); - } - for (const id of testData.expected.skippedJobIds) { - await ml.jobTable.filterWithSearchString(id, 0); + if (testData.expected.jobType === 'anomaly-detector') { + await ml.navigation.navigateToStackManagementJobsListPageAnomalyDetectionTab(); + await ml.jobTable.refreshJobList(); + for (const id of testData.expected.jobIds) { + await ml.jobTable.filterWithSearchString(id); + } + for (const id of testData.expected.skippedJobIds) { + await ml.jobTable.filterWithSearchString(id, 0); + } + } else { + await ml.navigation.navigateToStackManagementJobsListPageAnalyticsTab(); + await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); + for (const id of testData.expected.jobIds) { + await ml.dataFrameAnalyticsTable.assertAnalyticsJobDisplayedInTable(id, true); + } + for (const id of testData.expected.skippedJobIds) { + await ml.dataFrameAnalyticsTable.assertAnalyticsJobDisplayedInTable(id, false); + } } }); } diff --git a/x-pack/test/functional/services/ml/stack_management_jobs.ts b/x-pack/test/functional/services/ml/stack_management_jobs.ts index 30221f07e1cb2..8571f3a6a19a9 100644 --- a/x-pack/test/functional/services/ml/stack_management_jobs.ts +++ b/x-pack/test/functional/services/ml/stack_management_jobs.ts @@ -219,8 +219,8 @@ export function MachineLearningStackManagementJobsProvider( }, async assertJobIdsExist(expectedJobIds: string[]) { - const subj = await testSubjects.find('mlJobMgmtImportJobsFileRead'); - const inputs = await subj.findAllByTagName('input'); + const inputs = await testSubjects.findAll('mlJobMgmtImportJobIdInput'); + // const inputs = await subj.findAllByTagName('input'); const actualJobIds = await Promise.all(inputs.map((i) => i.getAttribute('value'))); expect(actualJobIds.sort()).to.eql( @@ -232,14 +232,18 @@ export function MachineLearningStackManagementJobsProvider( }, async assertCorrectTitle(jobCount: number, jobType: JobType) { - const subj = await testSubjects.find('mlJobMgmtImportJobsADTitle'); + const dataTestSubj = + jobType === 'anomaly-detector' + ? 'mlJobMgmtImportJobsADTitle' + : 'mlJobMgmtImportJobsDFATitle'; + const subj = await testSubjects.find(dataTestSubj); const title = (await subj.parseDomContent()).html(); const jobTypeString = jobType === 'anomaly-detector' ? 'anomaly detection' : 'data frame analytics'; const results = title.match( - /(\d) (anomaly detection|data frame analytics) jobs read from file$/ + /(\d) (anomaly detection|data frame analytics) job[s]? read from file$/ ); expect(results).to.not.eql(null, `Expected regex results to not be null`); const foundCount = results![1]; From 0a9b3508d9050ce98b6ec6cdc8537aa77fbb696c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 1 Sep 2021 13:11:57 +0100 Subject: [PATCH 04/10] removing export file --- .../ml/stack_management_jobs/export_jobs.ts | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts deleted file mode 100644 index cfa403af1a581..0000000000000 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import path from 'path'; - -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; - -export default function ({ getService }: FtrProviderContext) { - const ml = getService('ml'); - - describe('export jobs', function () { - this.tags(['mlqa']); - before(async () => { - await ml.testResources.setKibanaTimeZoneToUTC(); - - await ml.securityUI.loginAsMlPowerUser(); - await ml.navigation.navigateToStackManagement(); - }); - }); -} From dc41eeff671c815d1ab2cedbcdea5c4269f12569 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 1 Sep 2021 13:43:13 +0100 Subject: [PATCH 05/10] adds bad data test --- .../import_jobs_flyout/cannot_read_file_callout.tsx | 10 ++++++---- .../files_to_import/bad_data.json | 1 + .../apps/ml/stack_management_jobs/import_jobs.ts | 11 +++++++++++ .../functional/services/ml/stack_management_jobs.ts | 4 ++++ 4 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx index 4c7a2471db9d6..70f94d1e03155 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx @@ -21,10 +21,12 @@ export const CannotReadFileCallout: FC = () => { })} color="warning" > - +
+ +
); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json new file mode 100644 index 0000000000000..5c40480832c00 --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json @@ -0,0 +1 @@ +Hey! this isn't JSON. diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts index b1cb448f00a59..49c27f7b21a7b 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -93,5 +93,16 @@ export default function ({ getService }: FtrProviderContext) { } }); } + + describe('correctly fails to import bad data', async () => { + it('selects and reads file', async () => { + await ml.testExecution.logTestStep('selects job import'); + await ml.stackManagementJobs.openImportFlyout(); + await ml.stackManagementJobs.selectFileToImport( + path.join(__dirname, 'files_to_import', 'bad_data.json'), + true + ); + }); + }); }); } diff --git a/x-pack/test/functional/services/ml/stack_management_jobs.ts b/x-pack/test/functional/services/ml/stack_management_jobs.ts index 8571f3a6a19a9..225ee0c817543 100644 --- a/x-pack/test/functional/services/ml/stack_management_jobs.ts +++ b/x-pack/test/functional/services/ml/stack_management_jobs.ts @@ -288,5 +288,9 @@ export function MachineLearningStackManagementJobsProvider( await testSubjects.missingOrFail('mlJobMgmtImportJobsFlyout'); }); }, + + async assertReadErrorCalloutExists() { + await testSubjects.existOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + }, }; } From 6d3ea417a0c41491331596543f5b6d695992e142 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 1 Sep 2021 13:47:54 +0100 Subject: [PATCH 06/10] commented code --- .../test/functional/apps/ml/stack_management_jobs/import_jobs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts index 49c27f7b21a7b..5beb90cfdd033 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -8,7 +8,6 @@ import path from 'path'; import { FtrProviderContext } from '../../../ftr_provider_context'; -// import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; import { JobType } from '../../../../../plugins/ml/common/types/saved_objects'; export default function ({ getService }: FtrProviderContext) { From 9ac5c092dcc9b04de418a18ce43f0edfa93f032c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 2 Sep 2021 12:02:02 +0100 Subject: [PATCH 07/10] adding export job tests --- .../export_jobs_flyout/export_jobs_flyout.tsx | 104 +++--- .../ml/stack_management_jobs/export_jobs.ts | 314 ++++++++++++++++++ .../apps/ml/stack_management_jobs/index.ts | 1 + .../services/ml/stack_management_jobs.ts | 140 +++++++- 4 files changed, 501 insertions(+), 58 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx index 0a23dc4a2d9c0..6aa35d147a0d1 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -63,6 +63,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { const [exporting, setExporting] = useState(false); const [selectedJobType, setSelectedJobType] = useState(currentTab); const [switchTabConfirmVisible, setSwitchTabConfirmVisible] = useState(false); + const [switchTabNextTab, setSwitchTabNextTab] = useState(currentTab); const { displayErrorToast, displaySuccessToast } = useMemo( () => toastNotificationServiceProvider(toasts), [toasts] @@ -170,16 +171,23 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { } } - const attemptTabSwitch = useCallback(() => { - // if the user has already selected some jobs, open a confirm modal - // rather than changing tabs - if (selectedJobIds.length > 0) { - setSwitchTabConfirmVisible(true); - return; - } + const attemptTabSwitch = useCallback( + (jobType: JobType) => { + if (jobType === selectedJobType) { + return; + } + // if the user has already selected some jobs, open a confirm modal + // rather than changing tabs + if (selectedJobIds.length > 0) { + setSwitchTabNextTab(jobType); + setSwitchTabConfirmVisible(true); + return; + } - switchTab(); - }, [selectedJobIds]); + switchTab(jobType); + }, + [selectedJobIds] + ); useEffect(() => { setSelectedJobDependencies( @@ -187,9 +195,9 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { ); }, [selectedJobIds]); - function switchTab() { - const jobType = - selectedJobType === 'anomaly-detector' ? 'data-frame-analytics' : 'anomaly-detector'; + function switchTab(jobType: JobType) { + // const jobType = + // selectedJobType === 'anomaly-detector' ? 'data-frame-analytics' : 'anomaly-detector'; setSwitchTabConfirmVisible(false); setSelectedJobIds([]); @@ -232,8 +240,9 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { attemptTabSwitch('anomaly-detector')} disabled={exporting} + data-test-subj="mlJobMgmtExportJobsADTab" > = ({ isDisabled, currentTab }) => { attemptTabSwitch('data-frame-analytics')} disabled={exporting} + data-test-subj="mlJobMgmtExportJobsDFATab" > = ({ isDisabled, currentTab }) => { ) : ( <> - + = ({ isDisabled, currentTab }) => { - {adJobIds.map((id) => ( -
- toggleSelectedJob(e.target.checked, id)} - /> - -
- ))} +
+ {adJobIds.map((id) => ( +
+ toggleSelectedJob(e.target.checked, id)} + /> + +
+ ))} +
)} @@ -289,7 +306,12 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { ) : ( <> - + = ({ isDisabled, currentTab }) => { - - {dfaJobIds.map((id) => ( -
- toggleSelectedJob(e.target.checked, id)} - /> - -
- ))} +
+ {dfaJobIds.map((id) => ( +
+ toggleSelectedJob(e.target.checked, id)} + /> + +
+ ))} +
)} @@ -334,6 +357,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { disabled={selectedJobIds.length === 0 || exporting === true} onClick={onExport} fill + data-test-subj="mlJobMgmtExportExportButton" > = ({ isDisabled, currentTab }) => { {switchTabConfirmVisible === true ? ( switchTab(switchTabNextTab)} /> ) : null} diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts new file mode 100644 index 0000000000000..8a84158ee9681 --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts @@ -0,0 +1,314 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import type { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; + +const testADJobs: Array<{ job: Job; datafeed: Datafeed }> = [ + { + // @ts-expect-error not full interface + job: { + job_id: 'fq_single_1_smv', + groups: ['farequote', 'automated', 'single-metric'], + description: 'mean(responsetime) on farequote dataset with 15m bucket span', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + }, + ], + influencers: [], + }, + analysis_limits: { + model_memory_limit: '10mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: true, + annotations_enabled: true, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + datafeed: { + datafeed_id: 'datafeed-fq_single_1_smv', + job_id: 'fq_single_1_smv', + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + }, + }, + { + // @ts-expect-error not full interface + job: { + job_id: 'fq_single_2_smv', + groups: ['farequote', 'automated', 'single-metric'], + description: 'low_mean(responsetime) on farequote dataset with 15m bucket span', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'low_mean(responsetime)', + function: 'low_mean', + field_name: 'responsetime', + }, + ], + influencers: ['responsetime'], + }, + analysis_limits: { + model_memory_limit: '11mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: true, + annotations_enabled: true, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + datafeed: { + datafeed_id: 'datafeed-fq_single_2_smv', + job_id: 'fq_single_2_smv', + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + }, + }, + { + // @ts-expect-error not full interface + job: { + job_id: 'fq_single_3_smv', + groups: ['farequote', 'automated', 'single-metric'], + description: 'high_mean(responsetime) on farequote dataset with 15m bucket span', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'high_mean(responsetime)', + function: 'high_mean', + field_name: 'responsetime', + }, + ], + influencers: ['responsetime'], + }, + analysis_limits: { + model_memory_limit: '11mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: true, + annotations_enabled: true, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + datafeed: { + datafeed_id: 'datafeed-fq_single_3_smv', + job_id: 'fq_single_3_smv', + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + }, + }, +]; + +const testDFAJobs: DataFrameAnalyticsConfig[] = [ + // @ts-expect-error not full interface + { + id: `bm_1_1`, + description: + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + source: { + index: ['ft_bank_marketing'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'user-bm_1_1', + results_field: 'ml', + }, + analysis: { + classification: { + prediction_field_name: 'test', + dependent_variable: 'y', + training_percent: 20, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '60mb', + allow_lazy_start: false, + }, + // @ts-expect-error not full interface + { + id: `ihp_1_2`, + description: 'This is the job description', + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'user-ihp_1_2', + results_field: 'ml', + }, + analysis: { + outlier_detection: {}, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '5mb', + }, + // @ts-expect-error not full interface + { + id: `egs_1_3`, + description: 'This is the job description', + source: { + index: ['ft_egs_regression'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'user-egs_1_3', + results_field: 'ml', + }, + analysis: { + regression: { + prediction_field_name: 'test', + dependent_variable: 'stab', + training_percent: 20, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '20mb', + }, +]; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('export jobs', function () { + this.tags(['mlqa']); + before(async () => { + await ml.api.cleanMlIndices(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/egs_regression'); + await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); + + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const { job, datafeed } of testADJobs) { + await ml.api.createAnomalyDetectionJob(job); + await ml.api.createDatafeed(datafeed); + } + for (const job of testDFAJobs) { + await ml.api.createDataFrameAnalyticsJob(job); + } + + await ml.securityUI.loginAsMlPowerUser(); + await ml.navigation.navigateToStackManagement(); + await ml.navigation.navigateToStackManagementJobsListPage(); + }); + after(async () => { + await ml.api.cleanMlIndices(); + ml.stackManagementJobs.deleteExportedFiles([ + 'anomaly_detection_jobs', + 'data_frame_analytics_jobs', + ]); + }); + + it('opens export flyout and exports anomaly detector jobs', async () => { + await ml.stackManagementJobs.openExportFlyout(); + await ml.stackManagementJobs.selectExportJobType('anomaly-detector'); + await ml.stackManagementJobs.selectExportJobSelectAll(); + await ml.stackManagementJobs.selectExportJobs(); + await ml.stackManagementJobs.assertExportedADJobsAreCorrect(testADJobs); + }); + + it('opens export flyout and exports data frame analytics jobs', async () => { + await ml.stackManagementJobs.openExportFlyout(); + await ml.stackManagementJobs.selectExportJobType('data-frame-analytics'); + await ml.stackManagementJobs.selectExportJobSelectAll(); + await ml.stackManagementJobs.selectExportJobs(); + await ml.stackManagementJobs.assertExportedDFAJobsAreCorrect(testDFAJobs); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts index 4dc7dc6d69930..c5e0728266bab 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./synchronize')); loadTestFile(require.resolve('./manage_spaces')); loadTestFile(require.resolve('./import_jobs')); + loadTestFile(require.resolve('./export_jobs')); }); } diff --git a/x-pack/test/functional/services/ml/stack_management_jobs.ts b/x-pack/test/functional/services/ml/stack_management_jobs.ts index 225ee0c817543..0209855989f0a 100644 --- a/x-pack/test/functional/services/ml/stack_management_jobs.ts +++ b/x-pack/test/functional/services/ml/stack_management_jobs.ts @@ -6,11 +6,16 @@ */ import expect from '@kbn/expect'; +import { REPO_ROOT } from '@kbn/utils'; +import fs from 'fs'; +import path from 'path'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { MlADJobTable } from './job_table'; -import { MlDFAJobTable } from './data_frame_analytics_table'; -import { JobType } from '../../../../plugins/ml/common/types/saved_objects'; +import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { MlADJobTable } from './job_table'; +import type { MlDFAJobTable } from './data_frame_analytics_table'; +import type { JobType } from '../../../../plugins/ml/common/types/saved_objects'; +import type { Job, Datafeed } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import type { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; type SyncFlyoutObjectType = | 'MissingObjects' @@ -206,9 +211,16 @@ export function MachineLearningStackManagementJobsProvider( }); }, - async selectFileToImport(path: string, expectError: boolean = false) { - log.debug(`Importing file '${path}' ...`); - await PageObjects.common.setFileInputPath(path); + async openExportFlyout() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('mlJobsExportButton', 1000); + await testSubjects.existOrFail('mlJobMgmtExportJobsFlyout'); + }); + }, + + async selectFileToImport(filePath: string, expectError: boolean = false) { + log.debug(`Importing file '${filePath}' ...`); + await PageObjects.common.setFileInputPath(filePath); if (expectError) { await testSubjects.existOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); @@ -220,7 +232,6 @@ export function MachineLearningStackManagementJobsProvider( async assertJobIdsExist(expectedJobIds: string[]) { const inputs = await testSubjects.findAll('mlJobMgmtImportJobIdInput'); - // const inputs = await subj.findAllByTagName('input'); const actualJobIds = await Promise.all(inputs.map((i) => i.getAttribute('value'))); expect(actualJobIds.sort()).to.eql( @@ -274,23 +285,116 @@ export function MachineLearningStackManagementJobsProvider( }, async importJobs() { - await retry.tryForTime(5000, async () => { - await testSubjects.click('mlJobMgmtImportImportButton', 1000); + await testSubjects.click('mlJobMgmtImportImportButton', 1000); + await testSubjects.missingOrFail('mlJobMgmtImportJobsFlyout', { timeout: 60 * 1000 }); + }, + + async assertReadErrorCalloutExists() { + await testSubjects.existOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + }, + + async selectExportJobType(jobType: JobType) { + if (jobType === 'anomaly-detector') { + await testSubjects.click('mlJobMgmtExportJobsADTab'); + await testSubjects.existOrFail('mlJobMgmtExportJobsADJobList'); + } else { + await testSubjects.click('mlJobMgmtExportJobsDFATab'); + await testSubjects.existOrFail('mlJobMgmtExportJobsDFAJobList'); + } + }, + + async selectExportJobSelectAll() { + await testSubjects.click('mlJobMgmtExportJobsSelectAllButton'); + }, + + async getDownload(filePath: string) { + return retry.tryForTime(5000, async () => { + expect(fs.existsSync(filePath)).to.be(true); + return fs.readFileSync(filePath).toString(); }); - await retry.tryForTime(40000, async () => { - await testSubjects.missingOrFail('mlJobMgmtImportJobsFlyout'); + }, + + getExportedFile(fileName: string) { + return path.resolve(REPO_ROOT, `target/functional-tests/downloads/${fileName}.json`); + }, + + deleteExportedFiles(fileNames: string[]) { + fileNames.forEach((file) => { + try { + fs.unlinkSync(this.getExportedFile(file)); + } catch (e) { + // it might not have been there to begin with + } }); }, - async assertImportedJobIdsExist(expectedJobIds: string[]) { - await retry.tryForTime(40000, async () => { - await testSubjects.click('mlRefreshJobListButton', 1000); - await testSubjects.missingOrFail('mlJobMgmtImportJobsFlyout'); + async selectExportJobs() { + await testSubjects.click('mlJobMgmtExportExportButton'); + }, + + async assertExportedADJobsAreCorrect(expectedJobs: Array<{ job: Job; datafeed: Datafeed }>) { + const file = JSON.parse( + await this.getDownload(this.getExportedFile('anomaly_detection_jobs')) + ); + const loadedFile = Array.isArray(file) ? file : [file]; + const sortedActualJobs = loadedFile.sort((a, b) => + a.job.job_id.localeCompare((a = b.job.job_id)) + ); + + const sortedExpectedJobs = expectedJobs.sort((a, b) => + a.job.job_id.localeCompare(b.job.job_id) + ); + expect(sortedActualJobs.length).to.eql( + sortedExpectedJobs.length, + `Expected length of exported jobs to be '${sortedExpectedJobs.length}' (got '${sortedActualJobs.length}')` + ); + + sortedExpectedJobs.forEach((expectedJob, i) => { + expect(expectedJob.job.job_id).to.eql( + sortedActualJobs[i].job.job_id, + `Expected job id to be '${expectedJob.job.job_id}' (got '${sortedActualJobs[i].job.job_id}')` + ); + expect(expectedJob.job.analysis_config.detectors.length).to.eql( + sortedActualJobs[i].job.analysis_config.detectors.length, + `Expected detectors length to be '${expectedJob.job.analysis_config.detectors.length}' (got '${sortedActualJobs[i].job.analysis_config.detectors.length}')` + ); + expect(expectedJob.job.analysis_config.detectors[0].function).to.eql( + sortedActualJobs[i].job.analysis_config.detectors[0].function, + `Expected first detector function to be '${expectedJob.job.analysis_config.detectors[0].function}' (got '${sortedActualJobs[i].job.analysis_config.detectors[0].function}')` + ); }); }, - async assertReadErrorCalloutExists() { - await testSubjects.existOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + async assertExportedDFAJobsAreCorrect(expectedJobs: DataFrameAnalyticsConfig[]) { + const file = JSON.parse( + await this.getDownload(this.getExportedFile('data_frame_analytics_jobs')) + ); + const loadedFile = Array.isArray(file) ? file : [file]; + const sortedActualJobs = loadedFile.sort((a, b) => a.id.localeCompare(b.id)); + + const sortedExpectedJobs = expectedJobs.sort((a, b) => a.id.localeCompare(b.id)); + + expect(sortedActualJobs.length).to.eql( + sortedExpectedJobs.length, + `Expected length of exported jobs to be '${sortedExpectedJobs.length}' (got '${sortedActualJobs.length}')` + ); + + sortedExpectedJobs.forEach((expectedJob, i) => { + expect(expectedJob.id).to.eql( + sortedActualJobs[i].id, + `Expected job id to be '${expectedJob.id}' (got '${sortedActualJobs[i].id}')` + ); + const expectedType = Object.keys(expectedJob.analysis)[0]; + const actualType = Object.keys(sortedActualJobs[i].analysis)[0]; + expect(expectedType).to.eql( + actualType, + `Expected job type to be '${expectedType}' (got '${actualType}')` + ); + expect(expectedJob.dest.index).to.eql( + sortedActualJobs[i].dest.index, + `Expected destination index to be '${expectedJob.dest.index}' (got '${sortedActualJobs[i].dest.index}')` + ); + }); }, }; } From 4d52e69dd8996f822d615357e21648be129dec0b Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 2 Sep 2021 12:08:58 +0100 Subject: [PATCH 08/10] adds version to file names --- ...y_detection_jobs.json => anomaly_detection_jobs_7.16.json} | 0 ...nalytics_jobs.json => data_frame_analytics_jobs_7.16.json} | 0 .../functional/apps/ml/stack_management_jobs/import_jobs.ts | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/{anomaly_detection_jobs.json => anomaly_detection_jobs_7.16.json} (100%) rename x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/{data_frame_analytics_jobs.json => data_frame_analytics_jobs_7.16.json} (100%) diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json similarity index 100% rename from x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs.json rename to x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json similarity index 100% rename from x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs.json rename to x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts index 5beb90cfdd033..6211885af0a2a 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -15,7 +15,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const testDataListPositive = [ { - filePath: path.join(__dirname, 'files_to_import', 'anomaly_detection_jobs.json'), + filePath: path.join(__dirname, 'files_to_import', 'anomaly_detection_jobs_7.16.json'), expected: { jobType: 'anomaly-detector' as JobType, jobIds: ['ad-test1', 'ad-test3'], @@ -23,7 +23,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, { - filePath: path.join(__dirname, 'files_to_import', 'data_frame_analytics_jobs.json'), + filePath: path.join(__dirname, 'files_to_import', 'data_frame_analytics_jobs_7.16.json'), expected: { jobType: 'data-frame-analytics' as JobType, jobIds: ['dfa-test1'], From 62e901212a167e8db5384455296567e8b13963a8 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 2 Sep 2021 16:22:44 +0100 Subject: [PATCH 09/10] improving tests --- .../export_jobs_flyout/export_jobs_flyout.tsx | 30 ++++++++---- .../ml/stack_management_jobs/export_jobs.ts | 4 +- .../services/ml/stack_management_jobs.ts | 46 ++++++++++++------- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx index 6aa35d147a0d1..d1ea48895a1f4 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -275,10 +275,17 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { isDisabled={isDisabled} data-test-subj="mlJobMgmtExportJobsSelectAllButton" > - + {selectedJobIds.length === adJobIds.length ? ( + + ) : ( + + )}
@@ -312,10 +319,17 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { isDisabled={isDisabled} data-test-subj="mlJobMgmtExportJobsSelectAllButton" > - + {selectedJobIds.length === dfaJobIds.length ? ( + + ) : ( + + )}
diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts index 8a84158ee9681..d7a563e8c355f 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts @@ -298,7 +298,7 @@ export default function ({ getService }: FtrProviderContext) { it('opens export flyout and exports anomaly detector jobs', async () => { await ml.stackManagementJobs.openExportFlyout(); await ml.stackManagementJobs.selectExportJobType('anomaly-detector'); - await ml.stackManagementJobs.selectExportJobSelectAll(); + await ml.stackManagementJobs.selectExportJobSelectAll('anomaly-detector'); await ml.stackManagementJobs.selectExportJobs(); await ml.stackManagementJobs.assertExportedADJobsAreCorrect(testADJobs); }); @@ -306,7 +306,7 @@ export default function ({ getService }: FtrProviderContext) { it('opens export flyout and exports data frame analytics jobs', async () => { await ml.stackManagementJobs.openExportFlyout(); await ml.stackManagementJobs.selectExportJobType('data-frame-analytics'); - await ml.stackManagementJobs.selectExportJobSelectAll(); + await ml.stackManagementJobs.selectExportJobSelectAll('data-frame-analytics'); await ml.stackManagementJobs.selectExportJobs(); await ml.stackManagementJobs.assertExportedDFAJobsAreCorrect(testDFAJobs); }); diff --git a/x-pack/test/functional/services/ml/stack_management_jobs.ts b/x-pack/test/functional/services/ml/stack_management_jobs.ts index 0209855989f0a..45b9fa2f29ccd 100644 --- a/x-pack/test/functional/services/ml/stack_management_jobs.ts +++ b/x-pack/test/functional/services/ml/stack_management_jobs.ts @@ -303,8 +303,19 @@ export function MachineLearningStackManagementJobsProvider( } }, - async selectExportJobSelectAll() { + async selectExportJobSelectAll(jobType: JobType) { await testSubjects.click('mlJobMgmtExportJobsSelectAllButton'); + const subjLabel = + jobType === 'anomaly-detector' + ? 'mlJobMgmtExportJobsADJobList' + : 'mlJobMgmtExportJobsDFAJobList'; + const subj = await testSubjects.find(subjLabel); + const inputs = await subj.findAllByTagName('input'); + const allInputValues = await Promise.all(inputs.map((input) => input.getAttribute('value'))); + expect(allInputValues.every((i) => i === 'on')).to.eql( + true, + `Expected all inputs to be checked` + ); }, async getDownload(filePath: string) { @@ -330,6 +341,7 @@ export function MachineLearningStackManagementJobsProvider( async selectExportJobs() { await testSubjects.click('mlJobMgmtExportExportButton'); + await testSubjects.missingOrFail('mlJobMgmtExportJobsFlyout', { timeout: 60 * 1000 }); }, async assertExportedADJobsAreCorrect(expectedJobs: Array<{ job: Job; datafeed: Datafeed }>) { @@ -337,9 +349,7 @@ export function MachineLearningStackManagementJobsProvider( await this.getDownload(this.getExportedFile('anomaly_detection_jobs')) ); const loadedFile = Array.isArray(file) ? file : [file]; - const sortedActualJobs = loadedFile.sort((a, b) => - a.job.job_id.localeCompare((a = b.job.job_id)) - ); + const sortedActualJobs = loadedFile.sort((a, b) => a.job.job_id.localeCompare(b.job.job_id)); const sortedExpectedJobs = expectedJobs.sort((a, b) => a.job.job_id.localeCompare(b.job.job_id) @@ -350,18 +360,22 @@ export function MachineLearningStackManagementJobsProvider( ); sortedExpectedJobs.forEach((expectedJob, i) => { - expect(expectedJob.job.job_id).to.eql( - sortedActualJobs[i].job.job_id, + expect(sortedActualJobs[i].job.job_id).to.eql( + expectedJob.job.job_id, `Expected job id to be '${expectedJob.job.job_id}' (got '${sortedActualJobs[i].job.job_id}')` ); - expect(expectedJob.job.analysis_config.detectors.length).to.eql( - sortedActualJobs[i].job.analysis_config.detectors.length, + expect(sortedActualJobs[i].job.analysis_config.detectors.length).to.eql( + expectedJob.job.analysis_config.detectors.length, `Expected detectors length to be '${expectedJob.job.analysis_config.detectors.length}' (got '${sortedActualJobs[i].job.analysis_config.detectors.length}')` ); - expect(expectedJob.job.analysis_config.detectors[0].function).to.eql( - sortedActualJobs[i].job.analysis_config.detectors[0].function, + expect(sortedActualJobs[i].job.analysis_config.detectors[0].function).to.eql( + expectedJob.job.analysis_config.detectors[0].function, `Expected first detector function to be '${expectedJob.job.analysis_config.detectors[0].function}' (got '${sortedActualJobs[i].job.analysis_config.detectors[0].function}')` ); + expect(sortedActualJobs[i].datafeed.datafeed_id).to.eql( + expectedJob.datafeed.datafeed_id, + `Expected job id to be '${expectedJob.datafeed.datafeed_id}' (got '${sortedActualJobs[i].datafeed.datafeed_id}')` + ); }); }, @@ -380,18 +394,18 @@ export function MachineLearningStackManagementJobsProvider( ); sortedExpectedJobs.forEach((expectedJob, i) => { - expect(expectedJob.id).to.eql( - sortedActualJobs[i].id, + expect(sortedActualJobs[i].id).to.eql( + expectedJob.id, `Expected job id to be '${expectedJob.id}' (got '${sortedActualJobs[i].id}')` ); const expectedType = Object.keys(expectedJob.analysis)[0]; const actualType = Object.keys(sortedActualJobs[i].analysis)[0]; - expect(expectedType).to.eql( - actualType, + expect(actualType).to.eql( + expectedType, `Expected job type to be '${expectedType}' (got '${actualType}')` ); - expect(expectedJob.dest.index).to.eql( - sortedActualJobs[i].dest.index, + expect(sortedActualJobs[i].dest.index).to.eql( + expectedJob.dest.index, `Expected destination index to be '${expectedJob.dest.index}' (got '${sortedActualJobs[i].dest.index}')` ); }); From 538cb1b3751088eade0a21dab325097873bc50f9 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 2 Sep 2021 16:28:47 +0100 Subject: [PATCH 10/10] removing comment --- .../export_jobs_flyout/export_jobs_flyout.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx index d1ea48895a1f4..509c74c359657 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -196,9 +196,6 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { }, [selectedJobIds]); function switchTab(jobType: JobType) { - // const jobType = - // selectedJobType === 'anomaly-detector' ? 'data-frame-analytics' : 'anomaly-detector'; - setSwitchTabConfirmVisible(false); setSelectedJobIds([]); setSelectedJobType(jobType);