From 7ad94e9c4bdac78fc71814939a27f7389ee6587a Mon Sep 17 00:00:00 2001 From: Adam Demjen Date: Fri, 1 Dec 2023 19:16:08 -0500 Subject: [PATCH] [Enterprise Search] Model state change error handling (#172409) ## Summary This PR adds error handling to model actions (deploy, start) in the ML model selection list. If either of these API calls fail, an error is displayed. The error stays on the screen until the flyout is reopened or another model action succeeds. Unfortunately there's no straightforward way to surface the root cause, so we display a generic error message to check the Kibana logs (which do contain the cause). Screenshot 2023-12-01 at 17 50 52 ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --- .../ml_inference/configure_pipeline.tsx | 19 +++++++++++++++++++ .../ml_inference/model_select_logic.test.ts | 17 +++++++++++++++++ .../ml_inference/model_select_logic.ts | 14 +++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx index 75a0269f643cf..e3f771d0ba7e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { useValues, useActions } from 'kea'; import { + EuiCallOut, EuiFieldText, EuiForm, EuiFormRow, @@ -28,6 +29,7 @@ import { IndexViewLogic } from '../../index_view_logic'; import { EMPTY_PIPELINE_CONFIGURATION, MLInferenceLogic } from './ml_inference_logic'; import { ModelSelect } from './model_select'; +import { ModelSelectLogic } from './model_select_logic'; import { PipelineSelectOption } from './pipeline_select_option'; const PIPELINE_SELECT_PLACEHOLDER_VALUE = 'pipeline_placeholder$$'; @@ -56,6 +58,7 @@ export const ConfigurePipeline: React.FC = () => { const { selectExistingPipeline, setInferencePipelineConfiguration } = useActions(MLInferenceLogic); const { ingestionMethod } = useValues(IndexViewLogic); + const { modelStateChangeError } = useValues(ModelSelectLogic); const { pipelineName } = configuration; const nameError = formErrors.pipelineName !== undefined && pipelineName.length > 0; @@ -133,6 +136,22 @@ export const ConfigurePipeline: React.FC = () => { } /> + {modelStateChangeError && ( + <> + + + {modelStateChangeError} + + + + )} { }); }); + describe('modelStateChangeError', () => { + it('gets error from API error response', () => { + const error = { + body: { + error: 'some-error', + message: 'some-error-message', + statusCode: 500, + }, + } as HttpError; + + StartModelApiLogic.actions.apiError(error); + + expect(ModelSelectLogic.values.modelStateChangeError).toEqual('some-error-message'); + }); + }); + describe('selectableModels', () => { it('gets models data from API response', () => { CachedFetchModelsApiLogic.actions.apiSuccess(FETCH_MODELS_API_DATA_RESPONSE); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts index 9f8c2b8b97612..5cfa2148203e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts @@ -9,6 +9,7 @@ import { kea, MakeLogicType } from 'kea'; import { HttpError, Status } from '../../../../../../../common/types/api'; import { MlModel } from '../../../../../../../common/types/ml'; +import { getErrorsFromHttpResponse } from '../../../../../shared/flash_messages/handle_api_errors'; import { CachedFetchModelsApiLogic, CachedFetchModlesApiLogicActions, @@ -25,16 +26,18 @@ import { export interface ModelSelectActions { createModel: (modelId: string) => { modelId: string }; + createModelError: CreateModelApiLogicActions['apiError']; createModelMakeRequest: CreateModelApiLogicActions['makeRequest']; createModelSuccess: CreateModelApiLogicActions['apiSuccess']; fetchModels: () => void; - fetchModelsMakeRequest: CachedFetchModlesApiLogicActions['makeRequest']; fetchModelsError: CachedFetchModlesApiLogicActions['apiError']; + fetchModelsMakeRequest: CachedFetchModlesApiLogicActions['makeRequest']; fetchModelsSuccess: CachedFetchModlesApiLogicActions['apiSuccess']; startPollingModels: CachedFetchModlesApiLogicActions['startPolling']; startModel: (modelId: string) => { modelId: string }; + startModelError: CreateModelApiLogicActions['apiError']; startModelMakeRequest: StartModelApiLogicActions['makeRequest']; startModelSuccess: StartModelApiLogicActions['apiSuccess']; } @@ -45,6 +48,7 @@ export interface ModelSelectValues { createModelStatus: Status; isLoading: boolean; isInitialLoading: boolean; + modelStateChangeError: string | undefined; modelsData: FetchModelsApiResponse | undefined; modelsStatus: Status; selectableModels: MlModel[]; @@ -118,6 +122,14 @@ export const ModelSelectLogic = kea createModelStatus === Status.LOADING || startModelStatus === Status.LOADING, ], + modelStateChangeError: [ + () => [selectors.createModelError, selectors.startModelError], + (createModelError?: HttpError, startModelError?: HttpError) => { + if (!createModelError && !startModelError) return undefined; + + return getErrorsFromHttpResponse(createModelError ?? startModelError!)[0]; + }, + ], selectableModels: [ () => [selectors.modelsData], (response: FetchModelsApiResponse) => response ?? [],