From e4fc5167052d6547f86c46441a6511700b1b5f21 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Mon, 28 Jul 2025 13:38:07 -0700 Subject: [PATCH 1/9] Cleanly handle error states for backend connection issues Signed-off-by: Tyler Ohlsen --- common/constants.ts | 2 + public/pages/workflows/workflows.tsx | 58 ++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index 53f0649d..1f9b96f4 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -291,6 +291,8 @@ export const CREATE_WORKFLOW_LINK = 'https://opensearch.org/docs/latest/automating-configurations/api/create-workflow/'; export const WORKFLOW_TUTORIAL_LINK = 'https://opensearch.org/docs/latest/automating-configurations/workflow-tutorial/'; +export const MAIN_PLUGIN_DOC_LINK = + 'https://docs.opensearch.org/latest/vector-search/ai-search/workflow-builder/'; export const NORMALIZATION_PROCESSOR_LINK = 'https://opensearch.org/docs/latest/search-plugins/search-pipelines/normalization-processor/'; export const GITHUB_FEEDBACK_LINK = diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index d0a4dcfb..6859f801 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useState, useMemo } from 'react'; import { RouteComponentProps, useLocation } from 'react-router-dom'; -import { escape } from 'lodash'; +import { escape, isEmpty } from 'lodash'; import { EuiPageHeader, EuiPage, @@ -18,6 +18,7 @@ import { EuiFlexItem, EuiEmptyPrompt, EuiButton, + EuiLink, } from '@elastic/eui'; import queryString from 'query-string'; import { useSelector } from 'react-redux'; @@ -33,6 +34,7 @@ import { AppState, searchWorkflows, useAppDispatch } from '../../store'; import { EmptyListMessage } from './empty_list_message'; import { FETCH_ALL_QUERY_LARGE, + MAIN_PLUGIN_DOC_LINK, OPENSEARCH_FLOW, PLUGIN_NAME, } from '../../../common'; @@ -96,9 +98,22 @@ export function Workflows(props: WorkflowsProps) { queryParams.dataSourceId ); const dataSourceVersion = useDataSourceVersion(dataSourceId); - const { workflows, loading } = useSelector( - (state: AppState) => state.workflows + const { + workflows, + loading, + errorMessage: errorMessageFlowFramework, + } = useSelector((state: AppState) => state.workflows); + const { errorMessage: errorMessageMl } = useSelector( + (state: AppState) => state.ml + ); + const { errorMessage: errorMessageOpenSearch } = useSelector( + (state: AppState) => state.opensearch ); + const connectionErrors = + !isEmpty(errorMessageFlowFramework) || + !isEmpty(errorMessageMl) || + !isEmpty(errorMessageOpenSearch); + const noWorkflows = Object.keys(workflows || {}).length === 0 && !loading; const { @@ -277,7 +292,42 @@ export function Workflows(props: WorkflowsProps) { pageTitle={pageTitleAndDescription} bottomBorder={false} /> - {dataSourceEnabled && dataSourceId === undefined ? ( + {!dataSourceEnabled && connectionErrors ? ( + + Error accessing cluster} + body={ +

+ Ensure your OpenSearch cluster is available and has the Flow + Framework and ML Commons plugins installed. +

+ } + actions={ + + + See documentation + + + } + /> +
+ ) : dataSourceEnabled && connectionErrors ? ( + + Incompatible data source} + body={

Ensure the data source has the latest ML features.

} + actions={ + + Manage data sources + + } + /> +
+ ) : dataSourceEnabled && dataSourceId === undefined ? ( Incompatible data source} From c81b14448e709a82e57bf3eee5cbe651cade0939 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Mon, 28 Jul 2025 13:48:44 -0700 Subject: [PATCH 2/9] only show if datasourceid is populated Signed-off-by: Tyler Ohlsen --- public/pages/workflows/workflows.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index 6859f801..5d162591 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -311,7 +311,9 @@ export function Workflows(props: WorkflowsProps) { } /> - ) : dataSourceEnabled && connectionErrors ? ( + ) : dataSourceEnabled && + dataSourceId !== undefined && + connectionErrors ? ( Incompatible data source} From 5fcd998879c575a99045704fe6473df297ef0cba Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 29 Jul 2025 09:27:35 -0700 Subject: [PATCH 3/9] tune wording, add comments Signed-off-by: Tyler Ohlsen --- public/pages/workflows/workflows.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index 5d162591..0aef6cb4 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -292,6 +292,10 @@ export function Workflows(props: WorkflowsProps) { pageTitle={pageTitleAndDescription} bottomBorder={false} /> + {/** + * Local cluster issues: could be due to cluster being down, permissions issues, + * and/or missing plugins. + */} {!dataSourceEnabled && connectionErrors ? ( - ) : dataSourceEnabled && + ) : // Remote cluster/datasource issues: datasource is down, permissions issues, + // and/or missing plugins or features. + dataSourceEnabled && dataSourceId !== undefined && connectionErrors ? ( Incompatible data source} - body={

Ensure the data source has the latest ML features.

} + body={ +

+ Ensure the data source is available and has the latest ML + features. +

+ } actions={ Date: Tue, 29 Jul 2025 09:32:30 -0700 Subject: [PATCH 4/9] update changelog Signed-off-by: Tyler Ohlsen --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47d93bb6..4b6f0b00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ## [Unreleased 3.2](https://github.com/opensearch-project/anomaly-detection/compare/2.x...HEAD) ### Features ### Enhancements +- Cleanly handle error states for backend connection issues ([#757](https://github.com/opensearch-project/dashboards-flow-framework/pull/757)) + ### Bug Fixes ### Infrastructure ### Documentation From 2f8177adfcd051ef5fe6739d169002a4ba9a97da Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 29 Jul 2025 09:45:09 -0700 Subject: [PATCH 5/9] cover one more case of ignorable error on ff search api Signed-off-by: Tyler Ohlsen --- server/routes/helpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/routes/helpers.ts b/server/routes/helpers.ts index 9bf7b568..b07d8571 100644 --- a/server/routes/helpers.ts +++ b/server/routes/helpers.ts @@ -57,6 +57,7 @@ function isDatasourceError(err: any) { export function isIgnorableError(error: any): boolean { return ( error.body?.error?.type === INDEX_NOT_FOUND_EXCEPTION || + error.body?.error?.caused_by?.type === INDEX_NOT_FOUND_EXCEPTION || error.body?.error === NO_MODIFICATIONS_FOUND_TEXT ); } From c91cf63d30632f6adedffe6fb034e2ae4d5bdded Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 29 Jul 2025 10:23:58 -0700 Subject: [PATCH 6/9] Handle edge case NPE Signed-off-by: Tyler Ohlsen --- .../new_workflow/quick_configure_modal.tsx | 115 +++++++++--------- 1 file changed, 60 insertions(+), 55 deletions(-) diff --git a/public/pages/workflows/new_workflow/quick_configure_modal.tsx b/public/pages/workflows/new_workflow/quick_configure_modal.tsx index 0f43db5a..684c80f8 100644 --- a/public/pages/workflows/new_workflow/quick_configure_modal.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_modal.tsx @@ -186,12 +186,16 @@ export function QuickConfigureModal(props: QuickConfigureModalProps) { undefined ); useEffect(() => { - setEmbeddingModelInterface( - models[quickConfigureFields?.embeddingModelId || '']?.interface - ); + if (!isEmpty(models)) { + setEmbeddingModelInterface( + models[quickConfigureFields?.embeddingModelId || '']?.interface + ); + } }, [models, quickConfigureFields?.embeddingModelId]); useEffect(() => { - setLLMInterface(models[quickConfigureFields?.llmId || '']?.interface); + if (!isEmpty(models)) { + setLLMInterface(models[quickConfigureFields?.llmId || '']?.interface); + } }, [models, quickConfigureFields?.llmId]); // Deployed models state @@ -330,56 +334,57 @@ export function QuickConfigureModal(props: QuickConfigureModalProps) { )} - {props.workflow?.ui_metadata?.type === WORKFLOW_TYPE.SEMANTIC_SEARCH_USING_SPARSE_ENCODERS ? ( - - setQuickConfigureFields({ - ...quickConfigureFields, - embeddingModelId: modelId, - }) - } - /> - ) : ( - - setQuickConfigureFields({ - ...quickConfigureFields, - embeddingModelId: modelId, - }) - } - /> - )} - - - )} + {props.workflow?.ui_metadata?.type === + WORKFLOW_TYPE.SEMANTIC_SEARCH_USING_SPARSE_ENCODERS ? ( + + setQuickConfigureFields({ + ...quickConfigureFields, + embeddingModelId: modelId, + }) + } + /> + ) : ( + + setQuickConfigureFields({ + ...quickConfigureFields, + embeddingModelId: modelId, + }) + } + /> + )} + + + )} {props.workflow?.ui_metadata?.type !== WORKFLOW_TYPE.CUSTOM && ( <> @@ -864,7 +869,7 @@ function updateIndexConfig( }; } if (fields.vectorField) { - properties[fields.vectorField] = + properties[fields.vectorField] = workflow_type !== WORKFLOW_TYPE.SEMANTIC_SEARCH_USING_SPARSE_ENCODERS ? { type: 'knn_vector', dimension: fields.embeddingLength || '' } : { type: 'rank_features' }; From e0494e7b4fd6fd86cdcfe67f58e4547280ab9adb Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 29 Jul 2025 11:27:50 -0700 Subject: [PATCH 7/9] update how health checks are done Signed-off-by: Tyler Ohlsen --- public/pages/workflows/workflows.tsx | 57 +++++++++++++++++++++------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index 0aef6cb4..4a7bd504 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -30,7 +30,12 @@ import { import { getApplication, getCore, getNavigationUI } from '../../services'; import { WorkflowList } from './workflow_list'; import { NewWorkflow } from './new_workflow'; -import { AppState, searchWorkflows, useAppDispatch } from '../../store'; +import { + AppState, + searchModels, + searchWorkflows, + useAppDispatch, +} from '../../store'; import { EmptyListMessage } from './empty_list_message'; import { FETCH_ALL_QUERY_LARGE, @@ -98,21 +103,45 @@ export function Workflows(props: WorkflowsProps) { queryParams.dataSourceId ); const dataSourceVersion = useDataSourceVersion(dataSourceId); - const { - workflows, - loading, - errorMessage: errorMessageFlowFramework, - } = useSelector((state: AppState) => state.workflows); - const { errorMessage: errorMessageMl } = useSelector( - (state: AppState) => state.ml - ); - const { errorMessage: errorMessageOpenSearch } = useSelector( - (state: AppState) => state.opensearch + const { workflows, loading } = useSelector( + (state: AppState) => state.workflows ); + + // run health checks on FF and ML commons, any time there is a new selected datasource (or none if MDS is disabled) + // block all user actions if there are failures executing the basic search APIs for either plugin. + const [ + flowFrameworkConnectionErrors, + setFlowFrameworkConnectionErrors, + ] = useState(false); + const [mlCommonsConnectionErrors, setMLCommonsConnectionErrors] = useState< + boolean + >(false); const connectionErrors = - !isEmpty(errorMessageFlowFramework) || - !isEmpty(errorMessageMl) || - !isEmpty(errorMessageOpenSearch); + flowFrameworkConnectionErrors || mlCommonsConnectionErrors; + useEffect(() => { + async function flowFrameworkHealthCheck() { + await dispatch( + searchWorkflows({ + apiBody: FETCH_ALL_QUERY_LARGE, + dataSourceId, + }) + ).then((resp: any) => { + setFlowFrameworkConnectionErrors(!isEmpty(resp.error)); + }); + } + async function mlCommonsHealthCheck() { + await dispatch( + searchModels({ + apiBody: FETCH_ALL_QUERY_LARGE, + dataSourceId, + }) + ).then((resp: any) => { + setMLCommonsConnectionErrors(!isEmpty(resp.error)); + }); + } + flowFrameworkHealthCheck(); + mlCommonsHealthCheck(); + }, [dataSourceId]); const noWorkflows = Object.keys(workflows || {}).length === 0 && !loading; From 8425a100d1f8dd4d9ac957c7adcb8b4b5370737c Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 30 Jul 2025 11:30:46 -0700 Subject: [PATCH 8/9] Fix mock; minor improvement to health check logic Signed-off-by: Tyler Ohlsen --- public/pages/workflows/workflows.test.tsx | 1 - public/pages/workflows/workflows.tsx | 8 +++++--- test/mocks/mock_core_services.ts | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/public/pages/workflows/workflows.test.tsx b/public/pages/workflows/workflows.test.tsx index ae131cef..e2fed82b 100644 --- a/public/pages/workflows/workflows.test.tsx +++ b/public/pages/workflows/workflows.test.tsx @@ -8,7 +8,6 @@ import { render, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; - import { BrowserRouter as Router, RouteComponentProps, diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index 4a7bd504..d06a1840 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -139,9 +139,11 @@ export function Workflows(props: WorkflowsProps) { setMLCommonsConnectionErrors(!isEmpty(resp.error)); }); } - flowFrameworkHealthCheck(); - mlCommonsHealthCheck(); - }, [dataSourceId]); + if (isDataSourceReady(dataSourceId)) { + flowFrameworkHealthCheck(); + mlCommonsHealthCheck(); + } + }, [dataSourceId, dataSourceEnabled]); const noWorkflows = Object.keys(workflows || {}).length === 0 && !loading; diff --git a/test/mocks/mock_core_services.ts b/test/mocks/mock_core_services.ts index 5b18db94..f8a925f4 100644 --- a/test/mocks/mock_core_services.ts +++ b/test/mocks/mock_core_services.ts @@ -41,4 +41,18 @@ export const mockCoreServices = { }), getHeaderActionMenu: () => jest.fn(), + + // Iteratively add mocked values as needed when rendering components that make these dispatch calls in unit testing. + getRouteService: () => ({ + searchWorkflows: () => { + return { + workflows: {}, + }; + }, + searchModels: () => { + return { + models: {}, + }; + }, + }), }; From dd45fa34dd329137e54df57297f745716e9a9e18 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 30 Jul 2025 14:43:50 -0700 Subject: [PATCH 9/9] address comments Signed-off-by: Tyler Ohlsen --- public/pages/workflows/workflows.tsx | 55 +++++++++++++++------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index d06a1840..1c2d5f49 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -18,7 +18,6 @@ import { EuiFlexItem, EuiEmptyPrompt, EuiButton, - EuiLink, } from '@elastic/eui'; import queryString from 'query-string'; import { useSelector } from 'react-redux'; @@ -119,29 +118,28 @@ export function Workflows(props: WorkflowsProps) { const connectionErrors = flowFrameworkConnectionErrors || mlCommonsConnectionErrors; useEffect(() => { - async function flowFrameworkHealthCheck() { - await dispatch( - searchWorkflows({ - apiBody: FETCH_ALL_QUERY_LARGE, - dataSourceId, - }) - ).then((resp: any) => { - setFlowFrameworkConnectionErrors(!isEmpty(resp.error)); - }); - } - async function mlCommonsHealthCheck() { - await dispatch( - searchModels({ - apiBody: FETCH_ALL_QUERY_LARGE, - dataSourceId, - }) - ).then((resp: any) => { - setMLCommonsConnectionErrors(!isEmpty(resp.error)); - }); + async function healthCheck() { + await Promise.all([ + dispatch( + searchWorkflows({ + apiBody: FETCH_ALL_QUERY_LARGE, + dataSourceId, + }) + ).then((resp: any) => { + setFlowFrameworkConnectionErrors(!isEmpty(resp.error)); + }), + dispatch( + searchModels({ + apiBody: FETCH_ALL_QUERY_LARGE, + dataSourceId, + }) + ).then((resp: any) => { + setMLCommonsConnectionErrors(!isEmpty(resp.error)); + }), + ]); } if (isDataSourceReady(dataSourceId)) { - flowFrameworkHealthCheck(); - mlCommonsHealthCheck(); + healthCheck(); } }, [dataSourceId, dataSourceEnabled]); @@ -338,10 +336,15 @@ export function Workflows(props: WorkflowsProps) {

} actions={ - - - See documentation - + + See documentation } />