From 6b5d9ae6d26dfe236600e0cce6a20b26cbd624c1 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 16 Dec 2020 15:03:47 +0100 Subject: [PATCH 01/10] [ML] Add validation warning when cardinality checks don't return results. --- .../plugins/ml/common/constants/messages.ts | 32 +++++++++ .../job_validation/validate_cardinality.ts | 69 ++++++++++++------- 2 files changed, 76 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts index 1027ee5bf9a89..aa917ac7a0c28 100644 --- a/x-pack/plugins/ml/common/constants/messages.ts +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -90,6 +90,38 @@ export const getMessages = once(() => { url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-aggregation.html', }, + cardinality_no_results: { + status: VALIDATION_STATUS.WARNING, + heading: i18n.translate( + 'xpack.ml.models.jobValidation.messages.cardinalityNoResultsHeading', + { + defaultMessage: 'Field cardinality', + } + ), + text: i18n.translate('xpack.ml.models.jobValidation.messages.cardinalityNoResultsMessage', { + defaultMessage: `Cardinality checks could not be run. The query to validate fields didn't return any documents.`, + }), + }, + cardinality_field_not_exists: { + status: VALIDATION_STATUS.WARNING, + heading: i18n.translate( + 'xpack.ml.models.jobValidation.messages.cardinalityFieldNotExistsHeading', + { + defaultMessage: 'Field cardinality', + } + ), + text: i18n.translate( + 'xpack.ml.models.jobValidation.messages.cardinalityFieldNotExistsMessage', + { + defaultMessage: `Cardinality checks could not be run for field {fieldName}. The query to validate field didn't return any documents.`, + values: { + fieldName: '"{{fieldName}}"', + }, + } + ), + url: + 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-aggregation.html', + }, cardinality_by_field: { status: VALIDATION_STATUS.WARNING, text: i18n.translate('xpack.ml.models.jobValidation.messages.cardinalityByFieldMessage', { diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts index 822d1a1081874..3afb403554666 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts @@ -132,35 +132,54 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida datafeedConfig ); - uniqueFieldNames.forEach((uniqueFieldName) => { - const field = stats.aggregatableExistsFields.find( - (fieldData) => fieldData.fieldName === uniqueFieldName - ); - if (field !== undefined && typeof field === 'object' && field.stats) { - modelPlotCardinality += - modelPlotConfigFieldCount > 0 ? modelPlotConfigFieldCount : field.stats.cardinality!; - - if (isInvalid(field.stats.cardinality!)) { - messages.push({ - id: messageId || (`cardinality_${type}_field` as MessageId), - fieldName: uniqueFieldName, - }); - } - } else { - // only report uniqueFieldName as not aggregatable if it's not part - // of a valid categorization configuration and if it's not a scripted field or runtime mapping. - if ( - !isValidCategorizationConfig(job, uniqueFieldName) && - !isScriptField(job, uniqueFieldName) && - !isRuntimeMapping(job, uniqueFieldName) - ) { + if (stats.totalCount === 0) { + messages.push({ + id: 'cardinality_no_results', + }); + } else { + uniqueFieldNames.forEach((uniqueFieldName) => { + const aggregatableNotExistsField = stats.aggregatableNotExistsFields.find( + (fieldData) => fieldData.fieldName === uniqueFieldName + ); + + if (aggregatableNotExistsField !== undefined) { messages.push({ - id: 'field_not_aggregatable', + id: 'cardinality_field_not_exists', fieldName: uniqueFieldName, }); + } else { + const field = stats.aggregatableExistsFields.find( + (fieldData) => fieldData.fieldName === uniqueFieldName + ); + if (field !== undefined && typeof field === 'object' && field.stats) { + modelPlotCardinality += + modelPlotConfigFieldCount > 0 + ? modelPlotConfigFieldCount + : field.stats.cardinality!; + + if (isInvalid(field.stats.cardinality!)) { + messages.push({ + id: messageId || (`cardinality_${type}_field` as MessageId), + fieldName: uniqueFieldName, + }); + } + } else { + // only report uniqueFieldName as not aggregatable if it's not part + // of a valid categorization configuration and if it's not a scripted field or runtime mapping. + if ( + !isValidCategorizationConfig(job, uniqueFieldName) && + !isScriptField(job, uniqueFieldName) && + !isRuntimeMapping(job, uniqueFieldName) + ) { + messages.push({ + id: 'field_not_aggregatable', + fieldName: uniqueFieldName, + }); + } + } } - } - }); + }); + } } catch (e) { // checkAggregatableFieldsExist may return an error if 'fielddata' is // disabled for text fields (which is the default). If there was only From 8b7b876419cb3338b0ca7235fb1d197fb955b8c2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 16 Dec 2020 15:33:17 +0100 Subject: [PATCH 02/10] [ML] Add tests. --- .../validate_cardinality.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts index 2e2a9e21aa959..3996f42c48926 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts @@ -189,6 +189,38 @@ describe('ML - validateCardinality', () => { ); }); + it('cardinality no results', () => { + const job = getJobConfig('partition_field_name'); + + const mockCardinality = cloneDeep(mockResponses); + mockCardinality.search.hits.total.value = 0; + mockCardinality.search.aggregations.airline_count.doc_count = 0; + + return validateCardinality( + mlClusterClientFactory(mockCardinality), + (job as unknown) as CombinedJob + ).then((messages) => { + const ids = messages.map((m) => m.id); + expect(ids).toStrictEqual(['cardinality_no_results']); + }); + }); + + it('cardinality field not exists', () => { + const job = getJobConfig('partition_field_name'); + + const mockCardinality = cloneDeep(mockResponses); + mockCardinality.search.hits.total.value = 1; + mockCardinality.search.aggregations.airline_count.doc_count = 0; + + return validateCardinality( + mlClusterClientFactory(mockCardinality), + (job as unknown) as CombinedJob + ).then((messages) => { + const ids = messages.map((m) => m.id); + expect(ids).toStrictEqual(['cardinality_field_not_exists']); + }); + }); + it('fields not aggregatable', () => { const job = getJobConfig('partition_field_name'); job.analysis_config.detectors.push({ From 1d06c6a91738bcf8d2d061d03a105fd9df7bc9d2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 16 Dec 2020 18:43:10 +0100 Subject: [PATCH 03/10] [ML] Fix text. --- x-pack/plugins/ml/common/constants/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts index aa917ac7a0c28..0e7303c6d7317 100644 --- a/x-pack/plugins/ml/common/constants/messages.ts +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -113,7 +113,7 @@ export const getMessages = once(() => { text: i18n.translate( 'xpack.ml.models.jobValidation.messages.cardinalityFieldNotExistsMessage', { - defaultMessage: `Cardinality checks could not be run for field {fieldName}. The query to validate field didn't return any documents.`, + defaultMessage: `Cardinality checks could not be run for field {fieldName}. The query to validate the field didn't return any documents.`, values: { fieldName: '"{{fieldName}}"', }, From ded944e6cf33789af120bc8694bcfb2681b4c215 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 16 Dec 2020 19:49:02 +0100 Subject: [PATCH 04/10] [ML] Fix tests. --- .../ml/server/models/job_validation/job_validation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index d397d39d32b6b..8e88d1c1d0537 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -12,7 +12,7 @@ import type { MlClient } from '../../lib/ml_client'; const callAs = { fieldCaps: () => Promise.resolve({ body: { fields: [] } }), - search: () => Promise.resolve({ body: { hits: { total: { value: 0, relation: 'eq' } } } }), + search: () => Promise.resolve({ body: { hits: { total: { value: 1, relation: 'eq' } } } }), }; const mlClusterClient = ({ From 431710ef89f1d4968a498ac0f84e65912fcbf6f2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 17 Dec 2020 13:22:37 +0100 Subject: [PATCH 05/10] [ML] Skip validation without sample data. --- .../validate_job/validate_job_view.js | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index 0c079bc11cffc..37f058b415040 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -208,7 +208,7 @@ export class ValidateJobUI extends Component { const duration = typeof getDuration === 'function' ? getDuration() : undefined; const fields = this.props.fields; - if (typeof job === 'object') { + if (typeof job === 'object' && duration.start !== null && duration.end !== null) { let shouldShowLoadingIndicator = true; this.props.ml @@ -262,6 +262,33 @@ export class ValidateJobUI extends Component { }); } }, delay); + } else if (typeof job === 'object' && duration.start === null && duration.end === null) { + this.setState({ + ...this.state, + ui: { + ...this.state.ui, + iconType: statusToEuiIconType(VALIDATION_STATUS.WARNING), + isLoading: false, + isModalVisible: true, + }, + data: { + messages: [ + { + id: 'job_validation_skipped', + text: i18n.translate('xpack.ml.validateJob.jobValidationSkippedText', { + defaultMessage: + 'Job validation could not be run because of insufficient sample data.', + }), + status: VALIDATION_STATUS.WARNING, + }, + ], + success: true, + }, + title: job.job_id, + }); + if (typeof this.props.setIsValid === 'function') { + this.props.setIsValid(true); + } } }; From 371e1c9c2a525aa417ce759c1574e17919687eb8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 17 Dec 2020 13:33:57 +0100 Subject: [PATCH 06/10] [ML] Adds code comment. --- .../application/components/validate_job/validate_job_view.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index 37f058b415040..0855f728a7273 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -208,6 +208,8 @@ export class ValidateJobUI extends Component { const duration = typeof getDuration === 'function' ? getDuration() : undefined; const fields = this.props.fields; + // Run job validation only if a job config has been passed on and the duration makes sense to run it. + // Otherwise we skip the call and display a generic warning, but let the user move on to the next wizard step. if (typeof job === 'object' && duration.start !== null && duration.end !== null) { let shouldShowLoadingIndicator = true; From 6a39e2a58788e7ce0d8e4638c52c9c21bbeee4d7 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 17 Dec 2020 14:05:42 +0100 Subject: [PATCH 07/10] [ML] Add warning. --- .../application/components/validate_job/validate_job_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index 0855f728a7273..6e2b8391e3ba2 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -279,7 +279,7 @@ export class ValidateJobUI extends Component { id: 'job_validation_skipped', text: i18n.translate('xpack.ml.validateJob.jobValidationSkippedText', { defaultMessage: - 'Job validation could not be run because of insufficient sample data.', + 'Job validation could not be run because of insufficient sample data. Please be aware the this job may encounter problems when running.', }), status: VALIDATION_STATUS.WARNING, }, From b36624192f7d724b1e4b2bba68f76c25dab2ae0c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 17 Dec 2020 14:50:13 +0100 Subject: [PATCH 08/10] [ML] Tweak validation run check. --- .../validate_job/validate_job_view.js | 152 +++++++++--------- 1 file changed, 77 insertions(+), 75 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index 6e2b8391e3ba2..a152ee14a5413 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -210,86 +210,88 @@ export class ValidateJobUI extends Component { // Run job validation only if a job config has been passed on and the duration makes sense to run it. // Otherwise we skip the call and display a generic warning, but let the user move on to the next wizard step. - if (typeof job === 'object' && duration.start !== null && duration.end !== null) { - let shouldShowLoadingIndicator = true; + if (typeof job === 'object') { + if (typeof duration === 'object' && duration.start !== null && duration.end !== null) { + let shouldShowLoadingIndicator = true; - this.props.ml - .validateJob({ duration, fields, job }) - .then((messages) => { - shouldShowLoadingIndicator = false; - this.setState({ - ...this.state, - ui: { - ...this.state.ui, - iconType: statusToEuiIconType(getMostSevereMessageStatus(messages)), - isLoading: false, - isModalVisible: true, - }, - data: { - messages, - success: true, - }, - title: job.job_id, - }); - if (typeof this.props.setIsValid === 'function') { - this.props.setIsValid( - messages.some((m) => m.status === VALIDATION_STATUS.ERROR) === false + this.props.ml + .validateJob({ duration, fields, job }) + .then((messages) => { + shouldShowLoadingIndicator = false; + this.setState({ + ...this.state, + ui: { + ...this.state.ui, + iconType: statusToEuiIconType(getMostSevereMessageStatus(messages)), + isLoading: false, + isModalVisible: true, + }, + data: { + messages, + success: true, + }, + title: job.job_id, + }); + if (typeof this.props.setIsValid === 'function') { + this.props.setIsValid( + messages.some((m) => m.status === VALIDATION_STATUS.ERROR) === false + ); + } + }) + .catch((error) => { + const { toasts } = this.props.kibana.services.notifications; + const toastNotificationService = toastNotificationServiceProvider(toasts); + toastNotificationService.displayErrorToast( + error, + i18n.translate('xpack.ml.jobService.validateJobErrorTitle', { + defaultMessage: 'Job Validation Error', + }) ); + }); + + // wait for 250ms before triggering the loading indicator + // to avoid flickering when there's a loading time below + // 250ms for the job validation data + const delay = 250; + setTimeout(() => { + if (shouldShowLoadingIndicator) { + this.setState({ + ...this.state, + ui: { + ...this.state.ui, + isLoading: true, + isModalVisible: false, + }, + }); } - }) - .catch((error) => { - const { toasts } = this.props.kibana.services.notifications; - const toastNotificationService = toastNotificationServiceProvider(toasts); - toastNotificationService.displayErrorToast( - error, - i18n.translate('xpack.ml.jobService.validateJobErrorTitle', { - defaultMessage: 'Job Validation Error', - }) - ); + }, delay); + } else { + this.setState({ + ...this.state, + ui: { + ...this.state.ui, + iconType: statusToEuiIconType(VALIDATION_STATUS.WARNING), + isLoading: false, + isModalVisible: true, + }, + data: { + messages: [ + { + id: 'job_validation_skipped', + text: i18n.translate('xpack.ml.validateJob.jobValidationSkippedText', { + defaultMessage: + 'Job validation could not be run because of insufficient sample data. Please be aware the this job may encounter problems when running.', + }), + status: VALIDATION_STATUS.WARNING, + }, + ], + success: true, + }, + title: job.job_id, }); - - // wait for 250ms before triggering the loading indicator - // to avoid flickering when there's a loading time below - // 250ms for the job validation data - const delay = 250; - setTimeout(() => { - if (shouldShowLoadingIndicator) { - this.setState({ - ...this.state, - ui: { - ...this.state.ui, - isLoading: true, - isModalVisible: false, - }, - }); + if (typeof this.props.setIsValid === 'function') { + this.props.setIsValid(true); } - }, delay); - } else if (typeof job === 'object' && duration.start === null && duration.end === null) { - this.setState({ - ...this.state, - ui: { - ...this.state.ui, - iconType: statusToEuiIconType(VALIDATION_STATUS.WARNING), - isLoading: false, - isModalVisible: true, - }, - data: { - messages: [ - { - id: 'job_validation_skipped', - text: i18n.translate('xpack.ml.validateJob.jobValidationSkippedText', { - defaultMessage: - 'Job validation could not be run because of insufficient sample data. Please be aware the this job may encounter problems when running.', - }), - status: VALIDATION_STATUS.WARNING, - }, - ], - success: true, - }, - title: job.job_id, - }); - if (typeof this.props.setIsValid === 'function') { - this.props.setIsValid(true); } } }; From ae33db644d65f2e470084a924010e9f825306673 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 17 Dec 2020 15:34:27 +0100 Subject: [PATCH 09/10] [ML] Job validation next button should always be available. --- .../validate_job/validate_job_view.js | 18 +++++++++++++++--- .../components/validation_step/validation.tsx | 10 ++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index a152ee14a5413..058fe18acfa18 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -218,6 +218,20 @@ export class ValidateJobUI extends Component { .validateJob({ duration, fields, job }) .then((messages) => { shouldShowLoadingIndicator = false; + + const messagesContainError = messages.some((m) => m.status === VALIDATION_STATUS.ERROR); + + if (messagesContainError) { + messages.push({ + id: 'job_validation_includes_error', + text: i18n.translate('xpack.ml.validateJob.jobValidationIncludesErrorText', { + defaultMessage: + 'Job validation has failed, but you can still continue and create the job. Please be aware the job may encounter problems when running.', + }), + status: VALIDATION_STATUS.WARNING, + }); + } + this.setState({ ...this.state, ui: { @@ -233,9 +247,7 @@ export class ValidateJobUI extends Component { title: job.job_id, }); if (typeof this.props.setIsValid === 'function') { - this.props.setIsValid( - messages.some((m) => m.status === VALIDATION_STATUS.ERROR) === false - ); + this.props.setIsValid(!messagesContainError); } }) .catch((error) => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx index 3bde32f40eeb5..224e2eacb21e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useContext, useState, useEffect } from 'react'; +import React, { Fragment, FC, useContext, useEffect } from 'react'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -21,7 +21,6 @@ const idFilterList = [ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobCreatorUpdate, jobValidator } = useContext(JobCreatorContext); - const [nextActive, setNextActive] = useState(false); if (jobCreator.type === JOB_TYPE.ADVANCED) { // for advanced jobs, ignore time range warning as the @@ -50,13 +49,8 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) }, []); // keep a record of the advanced validation in the jobValidator - // and disable the next button if any advanced checks have failed. - // note, it is not currently possible to get to a state where any of the - // advanced validation checks return an error because they are all - // caught in previous basic checks function setIsValid(valid: boolean) { jobValidator.advancedValid = valid; - setNextActive(valid); } return ( @@ -74,7 +68,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} next={() => setCurrentStep(WIZARD_STEPS.SUMMARY)} - nextActive={nextActive} + nextActive={true} /> )} From 18a348cb3d2d02307250ed4d535622fa73b8886f Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 17 Dec 2020 15:54:12 +0100 Subject: [PATCH 10/10] [ML] Fix tests. --- .../application/components/validate_job/validate_job_view.js | 2 +- .../components/validate_job/validate_job_view.test.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index 058fe18acfa18..1ce4188d8e063 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -292,7 +292,7 @@ export class ValidateJobUI extends Component { id: 'job_validation_skipped', text: i18n.translate('xpack.ml.validateJob.jobValidationSkippedText', { defaultMessage: - 'Job validation could not be run because of insufficient sample data. Please be aware the this job may encounter problems when running.', + 'Job validation could not be run because of insufficient sample data. Please be aware the job may encounter problems when running.', }), status: VALIDATION_STATUS.WARNING, }, diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js index f2f785d91dcac..7e473f12ee50d 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js @@ -27,6 +27,7 @@ const job = { }; const getJobConfig = () => job; +const getDuration = () => ({ start: 0, end: 1 }); function prepareTest(messages) { const p = Promise.resolve(messages); @@ -40,7 +41,9 @@ function prepareTest(messages) { }, }; - const component = ; + const component = ( + + ); const wrapper = shallowWithIntl(component);