Skip to content

Commit ea7d2c0

Browse files
authored
Refactor quick configure components; improve processor error handling (#604)
Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent 0f5cfe9 commit ea7d2c0

File tree

15 files changed

+793
-788
lines changed

15 files changed

+793
-788
lines changed

public/pages/workflow_detail/components/edit_workflow_metadata_modal.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function EditWorkflowMetadataModal(
8585
}
8686
)
8787
.required('Required') as yup.Schema,
88-
desription: yup
88+
description: yup
8989
.string()
9090
.min(0)
9191
.max(MAX_DESCRIPTION_LENGTH, 'Too long')

public/pages/workflow_detail/tools/errors/errors.tsx

+20-7
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
*/
55

66
import React from 'react';
7-
import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
8-
import { isEmpty } from 'lodash';
7+
import {
8+
EuiCodeBlock,
9+
EuiEmptyPrompt,
10+
EuiFlexItem,
11+
EuiSpacer,
12+
} from '@elastic/eui';
913

1014
interface ErrorsProps {
11-
errorMessage: string;
15+
errorMessages: string[];
1216
}
1317

1418
/**
@@ -18,12 +22,21 @@ interface ErrorsProps {
1822
export function Errors(props: ErrorsProps) {
1923
return (
2024
<>
21-
{isEmpty(props.errorMessage) ? (
25+
{props.errorMessages?.length === 0 ? (
2226
<EuiEmptyPrompt title={<h2>No errors</h2>} titleSize="s" />
2327
) : (
24-
<EuiCodeBlock fontSize="m" isCopyable={false}>
25-
{props.errorMessage}
26-
</EuiCodeBlock>
28+
<>
29+
{props.errorMessages.map((errorMessage, idx) => {
30+
return (
31+
<EuiFlexItem grow={false} key={idx}>
32+
<EuiSpacer size="m" />
33+
<EuiCodeBlock fontSize="m" isCopyable={false} paddingSize="s">
34+
{errorMessage}
35+
</EuiCodeBlock>
36+
</EuiFlexItem>
37+
);
38+
})}
39+
</>
2740
)}
2841
</>
2942
);

public/pages/workflow_detail/tools/query/query.tsx

-11
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,12 @@ import {
2929
import {
3030
AppState,
3131
searchIndex,
32-
setOpenSearchError,
3332
setSearchPipelineErrors,
3433
useAppDispatch,
3534
} from '../../../../store';
3635
import {
3736
containsEmptyValues,
3837
containsSameValues,
39-
formatSearchPipelineErrors,
4038
getDataSourceId,
4139
getPlaceholdersFromQuery,
4240
getSearchPipelineErrors,
@@ -225,15 +223,6 @@ export function Query(props: QueryProps) {
225223
errors: searchPipelineErrors,
226224
})
227225
);
228-
if (!isEmpty(searchPipelineErrors)) {
229-
dispatch(
230-
setOpenSearchError({
231-
error: `Error running search pipeline. ${formatSearchPipelineErrors(
232-
searchPipelineErrors
233-
)}`,
234-
})
235-
);
236-
}
237226
} else {
238227
setSearchPipelineErrors({ errors: {} });
239228
}

public/pages/workflow_detail/tools/tools.tsx

+38-18
Original file line numberDiff line numberDiff line change
@@ -44,35 +44,55 @@ const PANEL_TITLE = 'Inspect flows';
4444
* The base Tools component for performing ingest and search, viewing resources, and debugging.
4545
*/
4646
export function Tools(props: ToolsProps) {
47-
// error message state
47+
// error message states. Error may come from several different sources.
4848
const { opensearch, workflows } = useSelector((state: AppState) => state);
4949
const opensearchError = opensearch.errorMessage;
5050
const workflowsError = workflows.errorMessage;
51-
const [curErrorMessage, setCurErrorMessage] = useState<string>('');
51+
const {
52+
ingestPipeline: ingestPipelineErrors,
53+
searchPipeline: searchPipelineErrors,
54+
} = useSelector((state: AppState) => state.errors);
55+
const [curErrorMessages, setCurErrorMessages] = useState<string[]>([]);
5256

53-
// auto-navigate to errors tab if a new error has been set as a result of
54-
// executing OpenSearch or Flow Framework workflow APIs, or from the workflow state
55-
// (note that if provision/deprovision fails, there is no concrete exception returned at the API level -
56-
// it is just set in the workflow's error field when fetching workflow state)
57+
// Propagate any errors coming from opensearch API calls, including ingest/search pipeline verbose calls.
5758
useEffect(() => {
58-
setCurErrorMessage(opensearchError);
59-
if (!isEmpty(opensearchError)) {
60-
props.setSelectedTabId(INSPECTOR_TAB_ID.ERRORS);
59+
if (
60+
!isEmpty(opensearchError) ||
61+
!isEmpty(ingestPipelineErrors) ||
62+
!isEmpty(searchPipelineErrors)
63+
) {
64+
if (!isEmpty(opensearchError)) {
65+
setCurErrorMessages([opensearchError]);
66+
} else if (!isEmpty(ingestPipelineErrors)) {
67+
setCurErrorMessages([
68+
'Data not ingested. Errors found with the following ingest processor(s):',
69+
...Object.values(ingestPipelineErrors).map((value) => value.errorMsg),
70+
]);
71+
} else if (!isEmpty(searchPipelineErrors)) {
72+
setCurErrorMessages([
73+
'Errors found with the following search processor(s)',
74+
...Object.values(searchPipelineErrors).map((value) => value.errorMsg),
75+
]);
76+
}
77+
} else {
78+
setCurErrorMessages([]);
6179
}
62-
}, [opensearchError]);
80+
}, [opensearchError, ingestPipelineErrors, searchPipelineErrors]);
6381

82+
// Propagate any errors coming from the workflow, either runtime from API call, or persisted in the indexed workflow itself.
6483
useEffect(() => {
65-
setCurErrorMessage(workflowsError);
66-
if (!isEmpty(workflowsError)) {
67-
props.setSelectedTabId(INSPECTOR_TAB_ID.ERRORS);
68-
}
84+
setCurErrorMessages(!isEmpty(workflowsError) ? [workflowsError] : []);
6985
}, [workflowsError]);
7086
useEffect(() => {
71-
setCurErrorMessage(props.workflow?.error || '');
72-
if (!isEmpty(props.workflow?.error)) {
87+
setCurErrorMessages(props.workflow?.error ? [props.workflow.error] : []);
88+
}, [props.workflow?.error]);
89+
90+
// auto-navigate to errors tab if new errors have been found
91+
useEffect(() => {
92+
if (curErrorMessages.length > 0) {
7393
props.setSelectedTabId(INSPECTOR_TAB_ID.ERRORS);
7494
}
75-
}, [props.workflow?.error]);
95+
}, [curErrorMessages]);
7696

7797
// auto-navigate to ingest tab if a populated value has been set, indicating ingest has been ran
7898
useEffect(() => {
@@ -136,7 +156,7 @@ export function Tools(props: ToolsProps) {
136156
/>
137157
)}
138158
{props.selectedTabId === INSPECTOR_TAB_ID.ERRORS && (
139-
<Errors errorMessage={curErrorMessage} />
159+
<Errors errorMessages={curErrorMessages} />
140160
)}
141161
{props.selectedTabId === INSPECTOR_TAB_ID.RESOURCES && (
142162
<Resources workflow={props.workflow} />

public/pages/workflow_detail/workflow_inputs/config_field_list.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ export function ConfigFieldList(props: ConfigFieldListProps) {
126126
el = (
127127
<EuiFlexItem key={idx}>
128128
<ModelField
129-
field={field}
130129
fieldPath={fieldPath}
131130
showMissingInterfaceCallout={false}
132131
/>

public/pages/workflow_detail/workflow_inputs/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
*/
55

66
export * from './workflow_inputs';
7+
export * from './input_fields';

public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx

+17-9
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,21 @@ import {
2121
MODEL_STATE,
2222
WorkflowFormValues,
2323
ModelFormValue,
24-
IConfigField,
2524
ML_CHOOSE_MODEL_LINK,
2625
ML_REMOTE_MODEL_LINK,
2726
} from '../../../../../common';
2827
import { AppState } from '../../../../store';
2928

3029
interface ModelFieldProps {
31-
field: IConfigField;
3230
fieldPath: string; // the full path in string-form to the field (e.g., 'ingest.enrich.processors.text_embedding_processor.inputField')
3331
hasModelInterface?: boolean;
3432
onModelChange?: (modelId: string) => void;
3533
showMissingInterfaceCallout?: boolean;
34+
label?: string;
35+
helpText?: string;
36+
fullWidth?: boolean;
37+
showError?: boolean;
38+
showInvalid?: boolean;
3639
}
3740

3841
type ModelItem = ModelFormValue & {
@@ -118,19 +121,28 @@ export function ModelField(props: ModelFieldProps) {
118121
)}
119122
<Field name={props.fieldPath}>
120123
{({ field, form }: FieldProps) => {
124+
const isInvalid =
125+
(props.showInvalid ?? true) &&
126+
getIn(errors, `${field.name}.id`) &&
127+
getIn(touched, `${field.name}.id`);
121128
return (
122129
<EuiCompressedFormRow
123-
label={'Model'}
130+
fullWidth={props.fullWidth}
131+
label={props.label || 'Model'}
124132
labelAppend={
125133
<EuiText size="xs">
126134
<EuiLink href={ML_CHOOSE_MODEL_LINK} target="_blank">
127135
Learn more
128136
</EuiLink>
129137
</EuiText>
130138
}
131-
helpText={'The model ID.'}
139+
helpText={props.helpText || 'The model ID.'}
140+
isInvalid={isInvalid}
141+
error={props.showError && getIn(errors, `${field.name}.id`)}
132142
>
133143
<EuiCompressedSuperSelect
144+
data-testid="selectDeployedModel"
145+
fullWidth={props.fullWidth}
134146
disabled={isEmpty(deployedModels)}
135147
options={deployedModels.map(
136148
(option) =>
@@ -165,11 +177,7 @@ export function ModelField(props: ModelFieldProps) {
165177
props.onModelChange(option);
166178
}
167179
}}
168-
isInvalid={
169-
getIn(errors, field.name) && getIn(touched, field.name)
170-
? true
171-
: undefined
172-
}
180+
isInvalid={isInvalid}
173181
/>
174182
</EuiCompressedFormRow>
175183
);

public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/ml_processor_inputs.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,6 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) {
200200
/>
201201
) : (
202202
<ModelField
203-
field={modelField}
204203
fieldPath={modelFieldPath}
205204
hasModelInterface={modelInterface !== undefined}
206205
onModelChange={onModelChange}

public/pages/workflow_detail/workflow_inputs/processors_list.tsx

+65-15
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
EuiAccordion,
1818
EuiSpacer,
1919
EuiText,
20-
EuiIconTip,
20+
EuiIcon,
21+
EuiCallOut,
2122
} from '@elastic/eui';
2223
import { cloneDeep, isEmpty } from 'lodash';
2324
import { getIn, useFormikContext } from 'formik';
@@ -85,6 +86,11 @@ export function ProcessorsList(props: ProcessorsListProps) {
8586
const [isPopoverOpen, setPopover] = useState(false);
8687
const [processors, setProcessors] = useState<IProcessorConfig[]>([]);
8788

89+
// Accordions do not persist an open/closed state, so we manually persist
90+
const [processorOpenState, setProcessorOpenState] = useState<{
91+
[processorId: string]: boolean;
92+
}>({});
93+
8894
function clearProcessorErrors(): void {
8995
if (props.context === PROCESSOR_CONTEXT.INGEST) {
9096
dispatch(setIngestPipelineErrors({ errors: {} }));
@@ -379,31 +385,60 @@ export function ProcessorsList(props: ProcessorsListProps) {
379385
`${processorIndex}.errorMsg`,
380386
undefined
381387
);
388+
const errorFound =
389+
processorFormError !== undefined ||
390+
processorRuntimeError !== undefined;
391+
const isAddedProcessor =
392+
processorAdded && processorIndex === processors.length - 1;
393+
const processorOpen =
394+
processorOpenState[processor.id] ?? isAddedProcessor;
395+
const errorMsg = processorFormError
396+
? 'Form error(s) detected'
397+
: 'Runtime error(s) detected';
382398

383399
return (
384400
<EuiFlexItem key={processorIndex}>
385401
<EuiPanel paddingSize="s">
386402
<EuiAccordion
387-
initialIsOpen={
388-
processorAdded && processorIndex === processors.length - 1
389-
}
403+
initialIsOpen={isAddedProcessor}
390404
id={`accordion${processor.id}`}
405+
onToggle={(isOpen) => {
406+
setProcessorOpenState({
407+
...processorOpenState,
408+
[processor.id]: isOpen,
409+
});
410+
}}
391411
buttonContent={
392-
<EuiFlexGroup direction="row">
412+
<EuiFlexGroup direction="column" gutterSize="none">
393413
<EuiFlexItem grow={false}>
394414
<EuiText>{`${processor.name || 'Processor'}`}</EuiText>
395415
</EuiFlexItem>
396-
{(processorFormError !== undefined ||
397-
processorRuntimeError !== undefined) && (
416+
{errorFound && !processorOpen && (
398417
<EuiFlexItem grow={false}>
399-
<EuiIconTip
400-
aria-label="Warning"
401-
size="m"
402-
type="alert"
403-
color="danger"
404-
content={processorFormError || processorRuntimeError}
405-
position="right"
406-
/>
418+
<EuiFlexGroup
419+
direction="row"
420+
gutterSize="s"
421+
alignItems="flexStart"
422+
>
423+
<EuiFlexItem grow={false}>
424+
<EuiIcon
425+
color="danger"
426+
type={'alert'}
427+
style={{ marginTop: '4px' }}
428+
/>
429+
</EuiFlexItem>
430+
<EuiFlexItem grow={false}>
431+
<EuiText
432+
size="s"
433+
color="danger"
434+
style={{
435+
marginTop: '0px',
436+
}}
437+
>
438+
{errorMsg}
439+
</EuiText>
440+
</EuiFlexItem>
441+
</EuiFlexGroup>
407442
</EuiFlexItem>
408443
)}
409444
</EuiFlexGroup>
@@ -421,6 +456,21 @@ export function ProcessorsList(props: ProcessorsListProps) {
421456
>
422457
<EuiSpacer size="s" />
423458
<EuiFlexItem style={{ paddingLeft: '28px' }}>
459+
{errorFound && processorOpen && (
460+
<>
461+
<EuiCallOut
462+
size="s"
463+
color="danger"
464+
iconType={'alert'}
465+
title={errorMsg}
466+
>
467+
<EuiText size="s">
468+
{processorFormError || processorRuntimeError}
469+
</EuiText>
470+
</EuiCallOut>
471+
<EuiSpacer size="s" />
472+
</>
473+
)}
424474
<ProcessorInputs
425475
uiConfig={props.uiConfig}
426476
config={processor}

0 commit comments

Comments
 (0)