From 9f7e6eb2fe4ab23d339e0ff41c66dc1e06675740 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 11 Jul 2024 09:06:33 -0700 Subject: [PATCH] Add error field to workflow; propagate all runtime errors in tools tab (#210) Signed-off-by: Tyler Ohlsen --- common/interfaces.ts | 2 + .../workflow_detail/tools/errors/errors.tsx | 19 +++++++-- public/pages/workflow_detail/tools/tools.tsx | 41 +++++++++++++++++-- .../pages/workflow_detail/workflow_detail.tsx | 12 +----- .../workflow_inputs/workflow_inputs.tsx | 6 +-- public/pages/workflows/workflows.tsx | 10 +---- public/store/reducers/opensearch_reducer.ts | 12 ++++++ public/store/reducers/workflows_reducer.ts | 2 +- public/utils/config_to_template_utils.ts | 1 + .../routes/flow_framework_routes_service.ts | 1 + server/routes/helpers.ts | 2 + 11 files changed, 76 insertions(+), 32 deletions(-) diff --git a/common/interfaces.ts b/common/interfaces.ts index 37d0c108..90311dca 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -310,6 +310,8 @@ export type Workflow = WorkflowTemplate & { // won't exist until launched/provisioned in backend state?: WORKFLOW_STATE; // won't exist until launched/provisioned in backend + error?: string; + // won't exist until launched/provisioned in backend resourcesCreated?: WorkflowResource[]; }; diff --git a/public/pages/workflow_detail/tools/errors/errors.tsx b/public/pages/workflow_detail/tools/errors/errors.tsx index 81ed584c..0493b1db 100644 --- a/public/pages/workflow_detail/tools/errors/errors.tsx +++ b/public/pages/workflow_detail/tools/errors/errors.tsx @@ -4,14 +4,27 @@ */ import React from 'react'; -import { EuiText } from '@elastic/eui'; +import { EuiCodeBlock, EuiText } from '@elastic/eui'; +import { isEmpty } from 'lodash'; -interface ErrorsProps {} +interface ErrorsProps { + errorMessage: string; +} /** * The basic errors component for the Tools panel. * Displays any errors found while users configure and test their workflow. */ export function Errors(props: ErrorsProps) { - return TODO: add errors details here; + return ( + <> + {isEmpty(props.errorMessage) ? ( + There are no errors. + ) : ( + + {props.errorMessage} + + )} + + ); } diff --git a/public/pages/workflow_detail/tools/tools.tsx b/public/pages/workflow_detail/tools/tools.tsx index e43282cc..f1d72fc3 100644 --- a/public/pages/workflow_detail/tools/tools.tsx +++ b/public/pages/workflow_detail/tools/tools.tsx @@ -4,6 +4,8 @@ */ import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from '../../../store'; import { isEmpty } from 'lodash'; import { EuiFlexGroup, @@ -59,16 +61,47 @@ const inputTabs = [ * The base Tools component for performing ingest and search, viewing resources, and debugging. */ export function Tools(props: ToolsProps) { + // error message state + const { opensearch, workflows } = useSelector((state: AppState) => state); + const opensearchError = opensearch.errorMessage; + const workflowsError = workflows.errorMessage; + const [curErrorMessage, setCurErrorMessage] = useState(''); + + // selected tab state const [selectedTabId, setSelectedTabId] = useState(TAB_ID.INGEST); - // auto-navigate to ingest response if a populated value has been set, indicating ingest has been ran + // auto-navigate to errors tab if a new error has been set as a result of + // executing OpenSearch or Flow Framework workflow APIs, or from the workflow state + // (note that if provision/deprovision fails, there is no concrete exception returned at the API level - + // it is just set in the workflow's error field when fetching workflow state) + useEffect(() => { + setCurErrorMessage(opensearchError); + if (!isEmpty(opensearchError)) { + setSelectedTabId(TAB_ID.ERRORS); + } + }, [opensearchError]); + + useEffect(() => { + setCurErrorMessage(workflowsError); + if (!isEmpty(workflowsError)) { + setSelectedTabId(TAB_ID.ERRORS); + } + }, [workflowsError]); + useEffect(() => { + setCurErrorMessage(props.workflow?.error || ''); + if (!isEmpty(props.workflow?.error)) { + setSelectedTabId(TAB_ID.ERRORS); + } + }, [props.workflow?.error]); + + // auto-navigate to ingest tab if a populated value has been set, indicating ingest has been ran useEffect(() => { if (!isEmpty(props.ingestResponse)) { setSelectedTabId(TAB_ID.INGEST); } }, [props.ingestResponse]); - // auto-navigate to query response if a populated value has been set, indicating search has been ran + // auto-navigate to query tab if a populated value has been set, indicating search has been ran useEffect(() => { if (!isEmpty(props.queryResponse)) { setSelectedTabId(TAB_ID.QUERY); @@ -114,7 +147,9 @@ export function Tools(props: ToolsProps) { {selectedTabId === TAB_ID.QUERY && ( )} - {selectedTabId === TAB_ID.ERRORS && } + {selectedTabId === TAB_ID.ERRORS && ( + + )} {selectedTabId === TAB_ID.RESOURCES && ( )} diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx index b9035c23..63fbc6b7 100644 --- a/public/pages/workflow_detail/workflow_detail.tsx +++ b/public/pages/workflow_detail/workflow_detail.tsx @@ -42,9 +42,7 @@ interface WorkflowDetailProps export function WorkflowDetail(props: WorkflowDetailProps) { const dispatch = useAppDispatch(); - const { workflows, errorMessage } = useSelector( - (state: AppState) => state.workflows - ); + const { workflows } = useSelector((state: AppState) => state.workflows); // selected workflow state const workflowId = props.match?.params?.workflowId; @@ -67,14 +65,6 @@ export function WorkflowDetail(props: WorkflowDetailProps) { dispatch(searchModels(FETCH_ALL_QUERY_BODY)); }, []); - // Show a toast if an error message exists in state - useEffect(() => { - if (errorMessage) { - console.error(errorMessage); - getCore().notifications.toasts.addDanger(errorMessage); - } - }, [errorMessage]); - return ( diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 3a9dbc72..6d26647e 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -224,7 +224,6 @@ export function WorkflowInputs(props: WorkflowInputsProps) { dispatch(removeDirty()); }) .catch((error: any) => { - getCore().notifications.toasts.addDanger(error); props.setIngestResponse(''); throw error; }); @@ -257,7 +256,6 @@ export function WorkflowInputs(props: WorkflowInputsProps) { dispatch(removeDirty()); }) .catch((error: any) => { - getCore().notifications.toasts.addDanger(error); props.setQueryResponse(''); throw error; }); @@ -334,9 +332,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { // @ts-ignore await dispatch(getWorkflow(props.workflow.id)); }) - .catch((error: any) => { - getCore().notifications.toasts.addDanger(error); - }) + .catch((error: any) => {}) .finally(() => { setIsModalOpen(false); }); diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index 8922b5b9..dda03517 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -54,7 +54,7 @@ function replaceActiveTab(activeTab: string, props: WorkflowsProps) { */ export function Workflows(props: WorkflowsProps) { const dispatch = useAppDispatch(); - const { workflows, loading, errorMessage } = useSelector( + const { workflows, loading } = useSelector( (state: AppState) => state.workflows ); @@ -92,14 +92,6 @@ export function Workflows(props: WorkflowsProps) { ]); }); - // Show a toast if an error message exists in state - useEffect(() => { - if (errorMessage) { - console.error(errorMessage); - getCore().notifications.toasts.addDanger(errorMessage); - } - }, [errorMessage]); - // On initial render: fetch all workflows useEffect(() => { dispatch(searchWorkflows(FETCH_ALL_QUERY_BODY)); diff --git a/public/store/reducers/opensearch_reducer.ts b/public/store/reducers/opensearch_reducer.ts index a9f71edc..44a92b06 100644 --- a/public/store/reducers/opensearch_reducer.ts +++ b/public/store/reducers/opensearch_reducer.ts @@ -89,6 +89,10 @@ const opensearchSlice = createSlice({ state.loading = true; state.errorMessage = ''; }) + .addCase(ingest.pending, (state, action) => { + state.loading = true; + state.errorMessage = ''; + }) .addCase(catIndices.fulfilled, (state, action) => { const indicesMap = new Map(); action.payload.forEach((index: Index) => { @@ -102,6 +106,10 @@ const opensearchSlice = createSlice({ state.loading = false; state.errorMessage = ''; }) + .addCase(ingest.fulfilled, (state, action) => { + state.loading = false; + state.errorMessage = ''; + }) .addCase(catIndices.rejected, (state, action) => { state.errorMessage = action.payload as string; state.loading = false; @@ -109,6 +117,10 @@ const opensearchSlice = createSlice({ .addCase(searchIndex.rejected, (state, action) => { state.errorMessage = action.payload as string; state.loading = false; + }) + .addCase(ingest.rejected, (state, action) => { + state.errorMessage = action.payload as string; + state.loading = false; }); }, }); diff --git a/public/store/reducers/workflows_reducer.ts b/public/store/reducers/workflows_reducer.ts index 7f169ed1..f4ccc509 100644 --- a/public/store/reducers/workflows_reducer.ts +++ b/public/store/reducers/workflows_reducer.ts @@ -4,7 +4,7 @@ */ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { Workflow, WorkflowDict, WorkflowTemplate } from '../../../common'; +import { WorkflowDict, WorkflowTemplate } from '../../../common'; import { HttpFetchError } from '../../../../../src/core/public'; import { getRouteService } from '../../services'; diff --git a/public/utils/config_to_template_utils.ts b/public/utils/config_to_template_utils.ts index 206441fc..3ff21934 100644 --- a/public/utils/config_to_template_utils.ts +++ b/public/utils/config_to_template_utils.ts @@ -243,6 +243,7 @@ export function reduceToTemplate(workflow: Workflow): WorkflowTemplate { lastUpdated, lastLaunched, state, + error, resourcesCreated, ...workflowTemplate } = workflow; diff --git a/server/routes/flow_framework_routes_service.ts b/server/routes/flow_framework_routes_service.ts index 7c60e631..45dea688 100644 --- a/server/routes/flow_framework_routes_service.ts +++ b/server/routes/flow_framework_routes_service.ts @@ -182,6 +182,7 @@ export class FlowFrameworkRoutesService { const workflowWithState = { ...workflow, state, + error: stateResponse.error, resourcesCreated, } as Workflow; return res.ok({ body: { workflow: workflowWithState } }); diff --git a/server/routes/helpers.ts b/server/routes/helpers.ts index 8833869d..74dcb60d 100644 --- a/server/routes/helpers.ts +++ b/server/routes/helpers.ts @@ -69,6 +69,7 @@ export function getWorkflowsFromResponses( const workflowState = getWorkflowStateFromResponse( workflowStateHit?._source?.state ); + const workflowError = workflowStateHit?._source?.error; const workflowResourcesCreated = getResourcesCreatedFromResponse( workflowStateHit?._source?.resources_created ); @@ -76,6 +77,7 @@ export function getWorkflowsFromResponses( ...workflowDict[workflowHit._id], // @ts-ignore state: workflowState, + error: workflowError, resourcesCreated: workflowResourcesCreated, }; });