From e5534cc45519e2865abc8d0bada524c2ddcc34b7 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 24 Aug 2023 13:19:35 -0600 Subject: [PATCH] [ML] Data Frame Analytics trained models: adds functional tests for 'Deploy Model' action (#163886) ## Summary Adds functional tests for deploy model action for DFA trained models with default config and with custom config. Part of https://github.com/elastic/kibana/issues/160712 Flaky test run: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2961 (updated) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit b2308a9005bc265438954e0bb7e66d4a409fbadc) --- .../add_inference_pipeline_flyout.tsx | 2 +- .../add_inference_pipeline_footer.tsx | 7 +- .../additional_advanced_settings.tsx | 8 +- .../components/on_failure_configuration.tsx | 6 +- .../components/pipeline_details.tsx | 3 + .../components/processor_configuration.tsx | 20 +- .../components/review_and_create_pipeline.tsx | 14 +- .../components/save_changes_button.tsx | 7 +- .../ml_inference/components/test_pipeline.tsx | 13 +- .../components/ml_inference/validation.ts | 2 +- .../model_management/model_actions.tsx | 6 +- .../model_management/model_list.ts | 158 ++++++++++ x-pack/test/functional/services/ml/api.ts | 6 +- .../services/ml/deploy_models_flyout.ts | 296 ++++++++++++++++++ x-pack/test/functional/services/ml/index.ts | 3 + .../services/ml/trained_models_table.ts | 41 +++ 16 files changed, 576 insertions(+), 16 deletions(-) create mode 100644 x-pack/test/functional/services/ml/deploy_models_flyout.ts diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx index 7d4ea408111fe..8c1d1cb4ec5c6 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx @@ -103,7 +103,7 @@ export const AddInferencePipelineFlyout: FC = ( ); return ( - +

diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx index f0a8beb2482f6..04ea2ea217375 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx @@ -47,7 +47,10 @@ export const AddInferencePipelineFooter: FC = ({ return ( - + {pipelineCreated ? CLOSE_BUTTON_LABEL : CANCEL_BUTTON_LABEL} @@ -66,6 +69,7 @@ export const AddInferencePipelineFooter: FC = ({ {nextStep !== undefined ? ( setStep(nextStep as AddInferencePipelineSteps)} @@ -76,6 +80,7 @@ export const AddInferencePipelineFooter: FC = ({ ) : ( = memo( return ( - + = memo( } > = memo( } > ) => handleAdditionalSettingsChange({ tag: e.target.value }) diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx index bc8bc4eedb2d3..f53e80b122f53 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx @@ -89,7 +89,7 @@ export const OnFailureConfiguration: FC = memo( }; return ( - + @@ -146,6 +146,7 @@ export const OnFailureConfiguration: FC = memo( = memo( {ignoreFailure === false ? ( = memo( <> {!editOnFailure ? ( = memo( ) : null} {editOnFailure ? ( = memo( isInvalid={pipelineNameError !== undefined} > = memo( = memo( > ) => handleConfigChange(e.target.value, 'targetField') diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/processor_configuration.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/processor_configuration.tsx index 7f2dfe9ede728..9da9f71b3cf96 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/processor_configuration.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/processor_configuration.tsx @@ -156,7 +156,10 @@ export const ProcessorConfiguration: FC = memo( }; return ( - + {/* INFERENCE CONFIG */} @@ -193,6 +196,7 @@ export const ProcessorConfiguration: FC = memo( { @@ -228,6 +232,7 @@ export const ProcessorConfiguration: FC = memo( } error={inferenceConfigError ?? inferenceConfigError} isInvalid={inferenceConfigError !== undefined || inferenceConfigError !== undefined} + data-test-subj="mlTrainedModelsInferencePipelineInferenceConfigEditor" > {editInferenceConfig ? ( = memo( onChange={handleInferenceConfigChange} /> ) : ( - + {JSON.stringify(inferenceConfig, null, 2)} )} @@ -311,6 +319,7 @@ export const ProcessorConfiguration: FC = memo( { @@ -349,10 +358,15 @@ export const ProcessorConfiguration: FC = memo( } error={fieldMapError} isInvalid={fieldMapError !== undefined} + data-test-subj="mlTrainedModelsInferencePipelineFieldMapEdit" > <> {!editFieldMapping ? ( - + {JSON.stringify(fieldMap ?? {}, null, 2)} ) : null} diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx index 352f11a0ba867..9e085b3e46509 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx @@ -58,7 +58,12 @@ export const ReviewAndCreatePipeline: FC = ({ const configCodeBlock = useMemo( () => ( - + {JSON.stringify(inferencePipeline ?? {}, null, 2)} ), @@ -67,7 +72,11 @@ export const ReviewAndCreatePipeline: FC = ({ return ( <> - + {pipelineCreated === false ? ( @@ -86,6 +95,7 @@ export const ReviewAndCreatePipeline: FC = ({ {pipelineCreated === true && pipelineError === undefined ? ( = ({ onClick, disabled }) => ( - + {i18n.translate( 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.saveChangesButton', { defaultMessage: 'Save changes' } diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx index 19120de441f4c..e2b5ceabaac88 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx @@ -129,7 +129,11 @@ export const TestPipeline: FC = memo(({ state, sourceIndex }) => { return ( <> - +

@@ -245,6 +249,7 @@ export const TestPipeline: FC = memo(({ state, sourceIndex }) => { <> = memo(({ state, sourceIndex }) => { - + {simulatePipelineError ? JSON.stringify(simulatePipelineError, null, 2) : simulatePipelineResult diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts b/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts index c86389607d54a..f6326669cf55b 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts +++ b/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts @@ -87,7 +87,7 @@ export const validateInferenceConfig = ( } // If populated, inference config must have the correct model type - if (inferenceConfig && inferenceConfigKeys.length > 0) { + if (modelType && inferenceConfig && inferenceConfigKeys.length > 0) { if (modelType === inferenceConfigKeys[0]) { return error; } else { diff --git a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx index 5f234a42c4734..2dc8e3f3ff5d5 100644 --- a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx +++ b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx @@ -456,7 +456,11 @@ export function useModelActions({ onModelDeployRequest(model); }, available: (item) => { - const isDfaTrainedModel = item.metadata?.analytics_config !== undefined; + const isDfaTrainedModel = + item.metadata?.analytics_config !== undefined || + item.inference_config?.regression !== undefined || + item.inference_config?.classification !== undefined; + return ( isDfaTrainedModel && !isBuiltInModel(item) && diff --git a/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts b/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts index de917a0b888d2..67f3728eab9f9 100644 --- a/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts +++ b/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { getDefaultOnFailureConfiguration } from '@kbn/ml-plugin/public/application/components/ml_inference/state'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { SUPPORTED_TRAINED_MODELS } from '../../../../services/ml/api'; @@ -37,6 +38,35 @@ export default function ({ getService }: FtrProviderContext) { modelTypes: ['regression', 'tree_ensemble'], }; + const modelWithoutPipelineDataExpectedValues = { + name: `ml-inference-${modelWithoutPipelineData.modelId}`, + duplicateName: `ml-inference-${modelWithoutPipelineData.modelId}-duplicate`, + description: `Uses the pre-trained data frame analytics model ${modelWithoutPipelineData.modelId} to infer against the data that is being ingested in the pipeline`, + duplicateDescription: 'Edited description', + inferenceConfig: { + regression: { + results_field: 'predicted_value', + num_top_feature_importance_values: 0, + }, + }, + inferenceConfigDuplicate: { + regression: { + results_field: 'predicted_value_for_duplicate', + num_top_feature_importance_values: 0, + }, + }, + editedInferenceConfig: { + regression: { + results_field: 'predicted_value_for_duplicate', + num_top_feature_importance_values: 0, + }, + }, + fieldMap: {}, + editedFieldMap: { + incoming_field: 'old_field', + }, + }; + before(async () => { for (const model of trainedModels) { await ml.api.importTrainedModel(model.id, model.name); @@ -52,6 +82,11 @@ export default function ({ getService }: FtrProviderContext) { after(async () => { await ml.api.stopAllTrainedModelDeploymentsES(); await ml.api.deleteAllTrainedModelsES(); + await ml.api.deleteIngestPipeline(modelWithoutPipelineDataExpectedValues.name, false); + await ml.api.deleteIngestPipeline( + modelWithoutPipelineDataExpectedValues.duplicateName, + false + ); await ml.api.cleanMlIndices(); }); @@ -118,6 +153,129 @@ export default function ({ getService }: FtrProviderContext) { await ml.trainedModelsTable.assertPipelinesTabContent(false); }); + it('deploys the trained model with default values', async () => { + await ml.testExecution.logTestStep('should display the trained model in the table'); + await ml.trainedModelsTable.filterWithSearchString(modelWithoutPipelineData.modelId, 1); + await ml.testExecution.logTestStep( + 'should not show collapsed actions menu for the model in the table' + ); + await ml.trainedModelsTable.assertModelCollapsedActionsButtonExists( + modelWithoutPipelineData.modelId, + false + ); + await ml.testExecution.logTestStep('should show deploy action for the model in the table'); + await ml.trainedModelsTable.assertModelDeployActionButtonExists( + modelWithoutPipelineData.modelId, + true + ); + await ml.testExecution.logTestStep('should open the deploy model flyout'); + await ml.trainedModelsTable.openTrainedModelsInferenceFlyout( + modelWithoutPipelineData.modelId + ); + await ml.testExecution.logTestStep('should complete the deploy model Details step'); + await ml.deployDFAModelFlyout.completeTrainedModelsInferenceFlyoutDetails({ + name: modelWithoutPipelineDataExpectedValues.name, + description: modelWithoutPipelineDataExpectedValues.description, + // If no metadata is provided, the target field will default to empty string + targetField: '', + }); + await ml.testExecution.logTestStep('should complete the deploy model Pipeline Config step'); + await ml.deployDFAModelFlyout.completeTrainedModelsInferenceFlyoutPipelineConfig({ + inferenceConfig: modelWithoutPipelineDataExpectedValues.inferenceConfig, + fieldMap: modelWithoutPipelineDataExpectedValues.fieldMap, + }); + await ml.testExecution.logTestStep( + 'should complete the deploy model pipeline On Failure step' + ); + await ml.deployDFAModelFlyout.completeTrainedModelsInferenceFlyoutOnFailure( + getDefaultOnFailureConfiguration() + ); + await ml.testExecution.logTestStep( + 'should complete the deploy model pipeline Create pipeline step' + ); + await ml.deployDFAModelFlyout.completeTrainedModelsInferenceFlyoutCreateStep({ + description: modelWithoutPipelineDataExpectedValues.description, + processors: [ + { + inference: { + model_id: modelWithoutPipelineData.modelId, + ignore_failure: false, + inference_config: modelWithoutPipelineDataExpectedValues.inferenceConfig, + on_failure: getDefaultOnFailureConfiguration(), + }, + }, + ], + }); + }); + + it('deploys the trained model with custom values', async () => { + await ml.testExecution.logTestStep('should display the trained model in the table'); + await ml.trainedModelsTable.filterWithSearchString(modelWithoutPipelineData.modelId, 1); + await ml.testExecution.logTestStep( + 'should not show collapsed actions menu for the model in the table' + ); + await ml.trainedModelsTable.assertModelCollapsedActionsButtonExists( + modelWithoutPipelineData.modelId, + false + ); + await ml.testExecution.logTestStep('should show deploy action for the model in the table'); + await ml.trainedModelsTable.assertModelDeployActionButtonExists( + modelWithoutPipelineData.modelId, + true + ); + await ml.testExecution.logTestStep('should open the deploy model flyout'); + await ml.trainedModelsTable.openTrainedModelsInferenceFlyout( + modelWithoutPipelineData.modelId + ); + await ml.testExecution.logTestStep('should complete the deploy model Details step'); + await ml.deployDFAModelFlyout.completeTrainedModelsInferenceFlyoutDetails( + { + name: modelWithoutPipelineDataExpectedValues.duplicateName, + description: modelWithoutPipelineDataExpectedValues.duplicateDescription, + targetField: 'myTargetField', + }, + true + ); + await ml.testExecution.logTestStep('should complete the deploy model Pipeline Config step'); + await ml.deployDFAModelFlyout.completeTrainedModelsInferenceFlyoutPipelineConfig( + { + inferenceConfig: modelWithoutPipelineDataExpectedValues.inferenceConfig, + editedInferenceConfig: modelWithoutPipelineDataExpectedValues.editedInferenceConfig, + fieldMap: modelWithoutPipelineDataExpectedValues.fieldMap, + editedFieldMap: modelWithoutPipelineDataExpectedValues.editedFieldMap, + }, + true + ); + await ml.testExecution.logTestStep( + 'should complete the deploy model pipeline On Failure step' + ); + await ml.deployDFAModelFlyout.completeTrainedModelsInferenceFlyoutOnFailure( + getDefaultOnFailureConfiguration(), + true + ); + await ml.testExecution.logTestStep( + 'should complete the deploy model pipeline Create pipeline step' + ); + await ml.deployDFAModelFlyout.completeTrainedModelsInferenceFlyoutCreateStep({ + description: modelWithoutPipelineDataExpectedValues.duplicateDescription, + processors: [ + { + inference: { + field_map: { + incoming_field: 'old_field', + }, + ignore_failure: true, + if: "ctx?.network?.name == 'Guest'", + model_id: modelWithoutPipelineData.modelId, + inference_config: modelWithoutPipelineDataExpectedValues.inferenceConfigDuplicate, + tag: 'tag', + target_field: 'myTargetField', + }, + }, + ], + }); + }); + it('displays the built-in model with only Test action enabled', async () => { await ml.testExecution.logTestStep('should display the model in the table'); await ml.trainedModelsTable.filterWithSearchString(builtInModelData.modelId, 1); diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 08799392bb37c..2536ec0bc564a 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -1498,9 +1498,11 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return ingestPipeline; }, - async deleteIngestPipeline(modelId: string) { + async deleteIngestPipeline(modelId: string, usePrefix: boolean = true) { log.debug(`Deleting ingest pipeline for trained model with id "${modelId}"`); - const { body, status } = await esSupertest.delete(`/_ingest/pipeline/pipeline_${modelId}`); + const { body, status } = await esSupertest.delete( + `/_ingest/pipeline/${usePrefix ? 'pipeline_' : ''}${modelId}` + ); this.assertResponseStatusCode(200, status, body); log.debug('> Ingest pipeline deleted'); diff --git a/x-pack/test/functional/services/ml/deploy_models_flyout.ts b/x-pack/test/functional/services/ml/deploy_models_flyout.ts new file mode 100644 index 0000000000000..ed52ce97c21f5 --- /dev/null +++ b/x-pack/test/functional/services/ml/deploy_models_flyout.ts @@ -0,0 +1,296 @@ +/* + * 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 expect from '@kbn/expect'; +import { IngestInferenceProcessor, IngestPipeline } from '@elastic/elasticsearch/lib/api/types'; +import { ProvidedType } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { MlCommonUI } from './common_ui'; + +export interface TrainedModelRowData { + id: string; + description: string; + modelTypes: string[]; +} + +export type MlDeployTrainedModelsFlyout = ProvidedType; + +export function DeployDFAModelFlyoutProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const find = getService('find'); + + return new (class DeployTrainedModelsFlyout { + public async setTrainedModelsInferenceFlyoutCustomValue(target: string, customValue: string) { + await mlCommonUI.setValueWithChecks(target, customValue, { + clearWithKeyboard: true, + }); + } + + public async setTrainedModelsInferenceFlyoutCustomDetails(values: { + name: string; + description: string; + targetField: string; + }) { + await this.setTrainedModelsInferenceFlyoutCustomValue( + 'mlTrainedModelsInferencePipelineNameInput', + values.name + ); + await this.setTrainedModelsInferenceFlyoutCustomValue( + 'mlTrainedModelsInferencePipelineDescriptionInput', + values.description + ); + await this.setTrainedModelsInferenceFlyoutCustomValue( + 'mlTrainedModelsInferencePipelineTargetFieldInput', + values.targetField + ); + } + + public async assertTrainedModelsInferenceFlyoutAdditionalSettings( + condition?: string, + tag?: string + ) { + if (condition || tag) { + await this.trainedModelsInferenceOpenAdditionalSettings(); + const actualCondition = await testSubjects.getAttribute( + 'mlTrainedModelsInferenceAdvancedSettingsConditionTextArea', + 'value' + ); + expect(actualCondition).to.eql(condition); + const actualTag = await testSubjects.getAttribute( + 'mlTrainedModelsInferenceAdvancedSettingsTagInput', + 'value' + ); + expect(actualTag).to.eql(tag); + } + } + + public async assertTrainedModelsInferenceFlyoutPipelineConfigValues( + inferenceConfig: IngestInferenceProcessor['inference_config'], + fieldMap: IngestInferenceProcessor['field_map'] + ) { + await retry.tryForTime(5000, async () => { + const actualInferenceConfig = await testSubjects.getVisibleText( + 'mlTrainedModelsInferencePipelineInferenceConfigBlock' + ); + expect(JSON.parse(actualInferenceConfig)).to.eql(inferenceConfig); + }); + + await retry.tryForTime(5000, async () => { + const actualFieldMap = await testSubjects.getVisibleText( + 'mlTrainedModelsInferencePipelineFieldMapBlock' + ); + expect(JSON.parse(actualFieldMap)).to.eql(fieldMap); + }); + } + + public async trainedModelsInferenceFlyoutSaveChanges() { + await testSubjects.existOrFail('mlTrainedModelsInferencePipelineFlyoutSaveChangesButton'); + const saveChangesButton = await testSubjects.find( + 'mlTrainedModelsInferencePipelineFlyoutSaveChangesButton' + ); + await saveChangesButton.click(); + } + + public async setTrainedModelsInferenceFlyoutCustomEditorValues( + selector: string, + value: string + ) { + const configElement = await testSubjects.find(selector); + const editor = await configElement.findByClassName('kibanaCodeEditor'); + await editor.click(); + const input = await find.activeElement(); + await input.clearValueWithKeyboard(); + + for (const chr of value) { + await retry.tryForTime(5000, async () => { + await input.type(chr, { charByChar: true }); + }); + } + } + + public async setTrainedModelsInferenceFlyoutCustomPipelineConfig(values: { + condition: string; + editedInferenceConfig: IngestInferenceProcessor['inference_config']; + editedFieldMap: IngestInferenceProcessor['field_map']; + tag: string; + }) { + // INFERENCE CONFIG + const editInferenceConfigButton = await testSubjects.find( + 'mlTrainedModelsInferencePipelineInferenceConfigEditButton' + ); + await editInferenceConfigButton.click(); + await this.setTrainedModelsInferenceFlyoutCustomEditorValues( + 'mlTrainedModelsInferencePipelineInferenceConfigEditor', + JSON.stringify(values.editedInferenceConfig) + ); + await this.trainedModelsInferenceFlyoutSaveChanges(); + // FIELD MAP + const editFieldMapButton = await testSubjects.find( + 'mlTrainedModelsInferencePipelineFieldMapEditButton' + ); + await editFieldMapButton.click(); + await this.setTrainedModelsInferenceFlyoutCustomEditorValues( + 'mlTrainedModelsInferencePipelineFieldMapEdit', + JSON.stringify(values.editedFieldMap) + ); + await this.trainedModelsInferenceFlyoutSaveChanges(); + // OPEN ADVANCED SETTINGS + await this.trainedModelsInferenceOpenAdditionalSettings(); + await this.setTrainedModelsInferenceFlyoutCustomValue( + 'mlTrainedModelsInferenceAdvancedSettingsConditionTextArea', + values.condition + ); + await this.setTrainedModelsInferenceFlyoutCustomValue( + 'mlTrainedModelsInferenceAdvancedSettingsTagInput', + values.tag + ); + await this.trainedModelsInferenceFlyoutSaveChanges(); + + await this.assertTrainedModelsInferenceFlyoutPipelineConfigValues( + values.editedInferenceConfig, + values.editedFieldMap + ); + await this.assertTrainedModelsInferenceFlyoutAdditionalSettings(values.condition, values.tag); + } + + public async completeTrainedModelsInferenceFlyoutDetails( + expectedValues: { + name: string; + description: string; + targetField: string; + }, + editDefaults: boolean = false + ) { + if (editDefaults) { + await this.setTrainedModelsInferenceFlyoutCustomDetails(expectedValues); + } + const name = await testSubjects.getAttribute( + 'mlTrainedModelsInferencePipelineNameInput', + 'value' + ); + expect(name).to.eql(expectedValues.name); + const description = await testSubjects.getAttribute( + 'mlTrainedModelsInferencePipelineDescriptionInput', + 'value' + ); + expect(description).to.eql(expectedValues.description); + const targetField = await testSubjects.getAttribute( + 'mlTrainedModelsInferencePipelineTargetFieldInput', + 'value' + ); + expect(targetField).to.eql(expectedValues.targetField); + await this.deployModelsContinue('mlTrainedModelsInferencePipelineProcessorConfigStep'); + } + + public async trainedModelsInferenceOpenAdditionalSettings() { + await testSubjects.existOrFail('mlTrainedModelsInferenceAdvancedSettingsAccordion'); + await testSubjects.click('mlTrainedModelsInferenceAdvancedSettingsAccordionButton'); + await testSubjects.existOrFail('mlTrainedModelsInferenceAdvancedSettingsConditionTextArea'); + await testSubjects.existOrFail('mlTrainedModelsInferenceAdvancedSettingsTagInput'); + } + + public async completeTrainedModelsInferenceFlyoutPipelineConfig( + expectedValues: { + inferenceConfig: IngestInferenceProcessor['inference_config']; + editedInferenceConfig?: IngestInferenceProcessor['inference_config']; + fieldMap: IngestInferenceProcessor['field_map']; + editedFieldMap?: IngestInferenceProcessor['field_map']; + }, + editDefaults: boolean = false + ) { + const { inferenceConfig, editedInferenceConfig, fieldMap, editedFieldMap } = expectedValues; + // Check all defaults + await this.assertTrainedModelsInferenceFlyoutPipelineConfigValues(inferenceConfig, fieldMap); + + if (editDefaults) { + await this.setTrainedModelsInferenceFlyoutCustomPipelineConfig({ + condition: "ctx?.network?.name == 'Guest'", + editedFieldMap, + editedInferenceConfig, + tag: 'tag', + }); + } + + await this.deployModelsContinue('mlTrainedModelsInferenceOnFailureStep'); + } + + public async completeTrainedModelsInferenceFlyoutOnFailure( + expectedOnFailure: IngestInferenceProcessor['on_failure'], + editDefaults: boolean = false + ) { + await retry.tryForTime(30 * 1000, async () => { + // Switch should default to unchecked + const ignoreFailureSelected = + (await testSubjects.getAttribute( + 'mlTrainedModelsInferenceIgnoreFailureSwitch', + 'aria-checked' + )) === 'true'; + expect(ignoreFailureSelected).to.eql(false); + // Switch should default to checked + const takeActionOnFailureSelected = + (await testSubjects.getAttribute( + 'mlTrainedModelsInferenceTakeActionOnFailureSwitch', + 'aria-checked' + )) === 'true'; + expect(takeActionOnFailureSelected).to.eql(true); + }); + + const defaultOnFailure = await testSubjects.getVisibleText( + 'mlTrainedModelsInferenceOnFailureCodeBlock' + ); + expect(JSON.parse(defaultOnFailure)).to.eql(expectedOnFailure); + + if (editDefaults) { + await retry.tryForTime(30 * 1000, async () => { + // switch ignore failure to true + await testSubjects.click('mlTrainedModelsInferenceIgnoreFailureSwitch'); + // Switch should now be checked + const isIgnoreFailureSelected = + (await testSubjects.getAttribute( + 'mlTrainedModelsInferenceIgnoreFailureSwitch', + 'aria-checked' + )) === 'true'; + expect(isIgnoreFailureSelected).to.eql(true); + }); + } + await this.deployModelsContinue('mlTrainedModelsInferenceTestStep'); + // skip test step + await this.deployModelsContinue('mlTrainedModelsInferenceReviewAndCreateStep'); + } + + public async completeTrainedModelsInferenceFlyoutCreateStep(expectedConfig: IngestPipeline) { + const pipelineConfig = await testSubjects.getVisibleText( + 'mlTrainedModelsInferenceReviewAndCreateStepConfigBlock' + ); + expect(JSON.parse(pipelineConfig)).to.eql(expectedConfig); + await this.assertDeployModelsCreateButton(); + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('mlTrainedModelsInferenceReviewAndCreateStepSuccessCallout'); + }); + const closeButton = await testSubjects.find('mlTrainedModelsInferencePipelineCloseButton'); + await closeButton.click(); + } + + public async deployModelsContinue(expectedStep?: string) { + await testSubjects.existOrFail('mlTrainedModelsInferencePipelineContinueButton'); + await testSubjects.click('mlTrainedModelsInferencePipelineContinueButton'); + if (expectedStep) { + await testSubjects.existOrFail(expectedStep); + } + } + + public async assertDeployModelsCreateButton(expectedStep?: string) { + await testSubjects.existOrFail('mlTrainedModelsInferencePipelineCreateButton'); + await testSubjects.click('mlTrainedModelsInferencePipelineCreateButton'); + } + })(); +} diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index b920b173e2080..ba36b3be3858f 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -57,6 +57,7 @@ import { MachineLearningDashboardJobSelectionTableProvider } from './dashboard_j import { MachineLearningDashboardEmbeddablesProvider } from './dashboard_embeddables'; import { TrainedModelsProvider } from './trained_models'; import { TrainedModelsTableProvider } from './trained_models_table'; +import { DeployDFAModelFlyoutProvider } from './deploy_models_flyout'; import { MachineLearningJobAnnotationsProvider } from './job_annotations_table'; import { MlNodesPanelProvider } from './ml_nodes_list'; import { MachineLearningCasesProvider } from './cases'; @@ -158,6 +159,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const swimLane = SwimLaneProvider(context); const trainedModels = TrainedModelsProvider(context, commonUI); const trainedModelsTable = TrainedModelsTableProvider(context, commonUI, trainedModels); + const deployDFAModelFlyout = DeployDFAModelFlyoutProvider(context, commonUI); const mlNodesPanel = MlNodesPanelProvider(context); const notifications = NotificationsProvider(context, commonUI, tableService); @@ -220,6 +222,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { testExecution, testResources, trainedModels, + deployDFAModelFlyout, trainedModelsTable, }; } diff --git a/x-pack/test/functional/services/ml/trained_models_table.ts b/x-pack/test/functional/services/ml/trained_models_table.ts index 9ca3a63615d93..fd993fe93a030 100644 --- a/x-pack/test/functional/services/ml/trained_models_table.ts +++ b/x-pack/test/functional/services/ml/trained_models_table.ts @@ -198,6 +198,19 @@ export function TrainedModelsTableProvider( ); } + public async assertModelDeployActionButtonExists(modelId: string, expectedValue: boolean) { + const actionsExists = await testSubjects.exists( + this.rowSelector(modelId, 'mlModelsTableRowDeployAction') + ); + + expect(actionsExists).to.eql( + expectedValue, + `Expected row deploy action button for trained model '${modelId}' to be ${ + expectedValue ? 'visible' : 'hidden' + } (got ${actionsExists ? 'visible' : 'hidden'})` + ); + } + public async assertModelTestButtonExists(modelId: string, expectedValue: boolean) { const actionExists = await testSubjects.exists( this.rowSelector(modelId, 'mlModelsTableRowTestAction') @@ -226,6 +239,15 @@ export function TrainedModelsTableProvider( await trainedModelsActions.testModelOutput(modelType, inputParams, expectedResult); } + public async openTrainedModelsInferenceFlyout(modelId: string) { + await mlCommonUI.invokeTableRowAction( + this.rowSelector(modelId), + 'mlModelsTableRowDeployAction', + false + ); + await this.assertDeployModelFlyoutExists(); + } + public async deleteModel(modelId: string) { await mlCommonUI.invokeTableRowAction( this.rowSelector(modelId), @@ -264,6 +286,19 @@ export function TrainedModelsTableProvider( ); } + public async deployModelsContinue(expectedStep?: string) { + await testSubjects.existOrFail('mlTrainedModelsInferencePipelineContinueButton'); + await testSubjects.click('mlTrainedModelsInferencePipelineContinueButton'); + if (expectedStep) { + await testSubjects.existOrFail(expectedStep); + } + } + + public async assertDeployModelsCreateButton(expectedStep?: string) { + await testSubjects.existOrFail('mlTrainedModelsInferencePipelineCreateButton'); + await testSubjects.click('mlTrainedModelsInferencePipelineCreateButton'); + } + public async assertDeleteModalExists() { await testSubjects.existOrFail('mlModelsDeleteModal', { timeout: 60 * 1000 }); } @@ -272,6 +307,12 @@ export function TrainedModelsTableProvider( await testSubjects.existOrFail('mlTestModelsFlyout', { timeout: 60 * 1000 }); } + public async assertDeployModelFlyoutExists() { + await testSubjects.existOrFail('mlTrainedModelsInferencePipelineFlyout', { + timeout: 60 * 1000, + }); + } + public async assertStartDeploymentModalExists(expectExist = true) { if (expectExist) { await testSubjects.existOrFail('mlModelsStartDeploymentModal', { timeout: 60 * 1000 });