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
}
/>