diff --git a/x-pack/plugins/ml/public/application/model_management/deployment_setup.tsx b/x-pack/plugins/ml/public/application/model_management/deployment_setup.tsx index 102af34d3e95d..7482f66fd4460 100644 --- a/x-pack/plugins/ml/public/application/model_management/deployment_setup.tsx +++ b/x-pack/plugins/ml/public/application/model_management/deployment_setup.tsx @@ -92,6 +92,7 @@ export const DeploymentSetup: FC = ({ id, label: id, value, + 'data-test-subj': `mlModelsStartDeploymentModalThreadsPerAllocation_${id}`, }; }), [maxSingleMlNodeProcessors] @@ -215,6 +216,7 @@ export const DeploymentSetup: FC = ({ defaultMessage: 'low', } ), + 'data-test-subj': 'mlModelsStartDeploymentModalLowPriority', }, { id: 'normal', @@ -225,6 +227,7 @@ export const DeploymentSetup: FC = ({ defaultMessage: 'normal', } ), + 'data-test-subj': 'mlModelsStartDeploymentModalNormalPriority', }, ]} data-test-subj={'mlModelsStartDeploymentModalPriority'} 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 ebcdfbeb4b170..c1c32114ea235 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 @@ -17,8 +17,7 @@ export default function ({ getService }: FtrProviderContext) { id: model.name, })); - // FLAKY: https://github.com/elastic/kibana/issues/165084 - describe.skip('trained models', function () { + describe('trained models', function () { // 'Created at' will be different on each run, // so we will just assert that the value is in the expected timestamp format. const builtInModelData = { @@ -112,12 +111,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.api.cleanMlIndices(); await ml.api.deleteIndices(modelWithPipelineAndDestIndexExpectedValues.index); - await ml.api.deleteIngestPipeline(modelWithoutPipelineDataExpectedValues.name, false); - await ml.api.deleteIngestPipeline( - modelWithoutPipelineDataExpectedValues.duplicateName, - false - ); - // Need to delete index before ingest pipeline, else it will give error await ml.api.deleteIngestPipeline(modelWithPipelineAndDestIndex.modelId); await ml.testResources.deleteDataViewByTitle( @@ -188,122 +181,141 @@ 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 show collapsed actions menu for the model in the table' - ); - await ml.trainedModelsTable.assertModelCollapsedActionsButtonExists( - modelWithoutPipelineData.modelId, - true - ); - await ml.testExecution.logTestStep('should show deploy action for the model in the table'); - await ml.trainedModelsTable.assertModelDeployActionButtonEnabled( - modelWithoutPipelineData.modelId, - true - ); - await ml.testExecution.logTestStep('should open the deploy model flyout'); - await ml.trainedModelsTable.clickDeployAction(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: '', + // FLAKY: https://github.com/elastic/kibana/issues/165084 + describe.skip('DFA model deployment', () => { + after(async () => { + await ml.api.deleteIngestPipeline(modelWithoutPipelineDataExpectedValues.name, false); + await ml.api.deleteIngestPipeline( + modelWithoutPipelineDataExpectedValues.duplicateName, + false + ); }); - 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, - true - ); - await ml.testExecution.logTestStep('should show deploy action for the model in the table'); - await ml.trainedModelsTable.assertModelDeployActionButtonExists( - modelWithoutPipelineData.modelId, - false - ); - await ml.testExecution.logTestStep('should open the deploy model flyout'); - await ml.trainedModelsTable.clickDeployAction(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( - { + it.skip('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 show collapsed actions menu for the model in the table' + ); + await ml.trainedModelsTable.assertModelCollapsedActionsButtonExists( + modelWithoutPipelineData.modelId, + true + ); + await ml.testExecution.logTestStep( + 'should show deploy action for the model in the table' + ); + await ml.trainedModelsTable.assertModelDeployActionButtonEnabled( + modelWithoutPipelineData.modelId, + true + ); + await ml.testExecution.logTestStep('should open the deploy model flyout'); + await ml.trainedModelsTable.clickDeployAction(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, - 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', + }); + 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(), }, - ignore_failure: true, - if: "ctx?.network?.name == 'Guest'", - model_id: modelWithoutPipelineData.modelId, - inference_config: modelWithoutPipelineDataExpectedValues.inferenceConfigDuplicate, - tag: 'tag', - target_field: 'myTargetField', }, + ], + }); + }); + + it.skip('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, + true + ); + await ml.testExecution.logTestStep( + 'should show deploy action for the model in the table' + ); + await ml.trainedModelsTable.assertModelDeployActionButtonExists( + modelWithoutPipelineData.modelId, + false + ); + await ml.testExecution.logTestStep('should open the deploy model flyout'); + await ml.trainedModelsTable.clickDeployAction(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', + }, + }, + ], + }); }); }); @@ -418,7 +430,7 @@ export default function ({ getService }: FtrProviderContext) { ); }); - it('navigates to data drift', async () => { + it.skip('navigates to data drift', async () => { await ml.testExecution.logTestStep('should show the model map in the expanded row'); await ml.trainedModelsTable.ensureRowIsExpanded(modelWithPipelineAndDestIndex.modelId); await ml.trainedModelsTable.assertModelsMapTabContent(); @@ -450,8 +462,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToTrainedModels(); }); - // FLAKY: https://github.com/elastic/kibana/issues/168899 - describe.skip('with imported models', function () { + describe('with imported models', function () { before(async () => { await ml.navigation.navigateToTrainedModels(); }); @@ -475,8 +486,10 @@ export default function ({ getService }: FtrProviderContext) { }); it(`stops deployment of the imported model ${model.id}`, async () => { + // Wait for the model to be deployed before stopping it. + await ml.testExecution.logTestStep('should have a Deployed state'); + await ml.trainedModelsTable.assertModelState(model.id, 'Deployed'); await ml.trainedModelsTable.stopDeployment(model.id); - await ml.trainedModelsTable.assertModelDeleteActionButtonEnabled(model.id, true); }); it(`deletes the imported model ${model.id}`, async () => { diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 14a0c8efc0589..dc4836ed6f34c 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -363,15 +363,21 @@ export function MachineLearningCommonUIProvider({ }); }, - async selectButtonGroupValue(inputTestSubj: string, value: string) { + async selectButtonGroupValue(inputTestSubj: string, value: string, valueTestSubj?: string) { await retry.tryForTime(5000, async () => { // The input element can not be clicked directly. // Instead, we need to click the corresponding label - const fieldSetElement = await testSubjects.find(inputTestSubj); + let labelElement: WebElementWrapper; - const labelElement = await fieldSetElement.findByCssSelector(`label[title="${value}"]`); - await labelElement.click(); + if (valueTestSubj) { + await testSubjects.click(valueTestSubj); + labelElement = await testSubjects.find(valueTestSubj); + } else { + const fieldSetElement = await testSubjects.find(inputTestSubj); + labelElement = await fieldSetElement.findByCssSelector(`label[title="${value}"]`); + await labelElement.click(); + } const labelClasses = await labelElement.getAttribute('class'); expect(labelClasses).to.contain( 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 1053d8a990e44..a186c531703be 100644 --- a/x-pack/test/functional/services/ml/trained_models_table.ts +++ b/x-pack/test/functional/services/ml/trained_models_table.ts @@ -52,6 +52,7 @@ export function TrainedModelsTableProvider( description: string; modelTypes: string[]; createdAt: string; + state: string; } = { id: $tr .findTestSubject('mlModelsTableColumnId') @@ -64,6 +65,11 @@ export function TrainedModelsTableProvider( .text() .trim(), modelTypes, + state: $tr + .findTestSubject('mlModelsTableColumnDeploymentState') + .find('.euiTableCellContent') + .text() + .trim(), createdAt: $tr .findTestSubject('mlModelsTableColumnCreatedAt') .find('.euiTableCellContent') @@ -193,12 +199,17 @@ export function TrainedModelsTableProvider( public async toggleActionsContextMenu(modelId: string, expectOpen = true) { await testSubjects.click(this.rowSelector(modelId, 'euiCollapsedItemActionsButton')); - const panelElement = await find.byCssSelector('.euiContextMenuPanel'); - const isDisplayed = await panelElement.isDisplayed(); - expect(isDisplayed).to.eql( - expectOpen, - `Expected the action context menu for '${modelId}' to be ${expectOpen ? 'open' : 'closed'}` - ); + + await retry.tryForTime(5 * 1000, async () => { + const panelElement = await find.byCssSelector('.euiContextMenuPanel'); + const isDisplayed = await panelElement.isDisplayed(); + expect(isDisplayed).to.eql( + expectOpen, + `Expected the action context menu for '${modelId}' to be ${ + expectOpen ? 'open' : 'closed' + }` + ); + }); } public async assertModelDeleteActionButtonExists(modelId: string, expectedValue: boolean) { @@ -510,14 +521,18 @@ export function TrainedModelsTableProvider( public async setPriority(value: 'low' | 'normal') { await mlCommonUI.selectButtonGroupValue( 'mlModelsStartDeploymentModalPriority', - value.toString() + value.toString(), + value === 'normal' + ? 'mlModelsStartDeploymentModalNormalPriority' + : 'mlModelsStartDeploymentModalLowPriority' ); } public async setThreadsPerAllocation(value: number) { await mlCommonUI.selectButtonGroupValue( 'mlModelsStartDeploymentModalThreadsPerAllocation', - value.toString() + value.toString(), + `mlModelsStartDeploymentModalThreadsPerAllocation_${value}` ); } @@ -540,6 +555,25 @@ export function TrainedModelsTableProvider( `Deployment for "${modelId}" has been started successfully.` ); await this.waitForModelsToLoad(); + + await retry.tryForTime( + 5 * 1000, + async () => { + await this.assertModelState(modelId, 'Deployed'); + }, + async () => { + await this.refreshModelsTable(); + } + ); + } + + public async assertModelState(modelId: string, expectedValue = 'Deployed') { + const rows = await this.parseModelsTable(); + const modelRow = rows.find((row) => row.id === modelId); + expect(modelRow?.state).to.eql( + expectedValue, + `Expected trained model row state to be '${expectedValue}' (got '${modelRow?.state!}')` + ); } public async stopDeployment(modelId: string) {