Skip to content

Commit 7140de2

Browse files
authored
UX fit-n-finish updates IX (#582)
Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent 3d6284f commit 7140de2

22 files changed

+421
-81
lines changed

common/constants.ts

+39-3
Original file line numberDiff line numberDiff line change
@@ -573,17 +573,52 @@ export enum PROCESSOR_CONTEXT {
573573
SEARCH_REQUEST = 'search_request',
574574
SEARCH_RESPONSE = 'search_response',
575575
}
576+
export const NO_TRANSFORMATION = 'No transformation';
576577
export enum TRANSFORM_CONTEXT {
577578
INPUT = 'input',
578579
OUTPUT = 'output',
579580
}
580581
export enum TRANSFORM_TYPE {
581582
STRING = 'String',
582583
FIELD = 'Field',
583-
EXPRESSION = 'Expression',
584-
TEMPLATE = 'Template',
584+
EXPRESSION = 'JSONPath Expression',
585+
TEMPLATE = 'Prompt',
585586
}
586-
export const NO_TRANSFORMATION = 'No transformation';
587+
588+
export const INPUT_TRANSFORM_OPTIONS = [
589+
{
590+
id: TRANSFORM_TYPE.FIELD,
591+
description: 'Map an existing field from your data.',
592+
},
593+
{
594+
id: TRANSFORM_TYPE.TEMPLATE,
595+
description: 'Configure a prompt and map to the input field.',
596+
},
597+
{
598+
id: TRANSFORM_TYPE.EXPRESSION,
599+
description: 'Extract data before mapping to the input field.',
600+
},
601+
{
602+
id: TRANSFORM_TYPE.STRING,
603+
description: 'Declare a string to the input field.',
604+
},
605+
];
606+
607+
export const OUTPUT_TRANSFORM_OPTIONS = [
608+
{
609+
id: TRANSFORM_TYPE.FIELD,
610+
description: 'Map an existing field from your data.',
611+
},
612+
{
613+
id: TRANSFORM_TYPE.EXPRESSION,
614+
description: 'Extract data before mapping to the input field.',
615+
},
616+
{
617+
id: NO_TRANSFORMATION,
618+
description: 'Leave the output field as-is.',
619+
},
620+
];
621+
587622
export const DEFAULT_NEW_WORKFLOW_NAME = 'new_workflow';
588623
export const DEFAULT_NEW_WORKFLOW_DESCRIPTION = 'My new workflow';
589624
export const DEFAULT_NEW_WORKFLOW_STATE_TYPE = ('NOT_STARTED' as any) as typeof WORKFLOW_STATE;
@@ -608,6 +643,7 @@ export enum SORT_ORDER {
608643
export const MAX_DOCS = 1000;
609644
export const MAX_DOCS_TO_IMPORT = 100;
610645
export const MAX_STRING_LENGTH = 100;
646+
export const MAX_DESCRIPTION_LENGTH = 1000;
611647
export const MAX_JSON_STRING_LENGTH = 10000;
612648
export const MAX_TEMPLATE_STRING_LENGTH = 10000;
613649
export const MAX_BYTES = 1048576; // OSD REST request payload size limit

common/interfaces.ts

+5
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,11 @@ export type QueryParam = {
563563
value: string;
564564
};
565565

566+
export type CachedFormikState = {
567+
errors?: {};
568+
touched?: {};
569+
};
570+
566571
/**
567572
********** OPENSEARCH TYPES/INTERFACES ************
568573
*/

public/pages/workflow_detail/components/edit_workflow_metadata_modal.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '@elastic/eui';
2121
import {
2222
FETCH_ALL_QUERY_LARGE,
23-
MAX_STRING_LENGTH,
23+
MAX_DESCRIPTION_LENGTH,
2424
Workflow,
2525
WORKFLOW_NAME_REGEXP,
2626
WORKFLOW_NAME_RESTRICTIONS,
@@ -87,9 +87,8 @@ export function EditWorkflowMetadataModal(
8787
.required('Required') as yup.Schema,
8888
desription: yup
8989
.string()
90-
.trim()
9190
.min(0)
92-
.max(MAX_STRING_LENGTH, 'Too long')
91+
.max(MAX_DESCRIPTION_LENGTH, 'Too long')
9392
.optional() as yup.Schema,
9493
}) as yup.Schema;
9594

public/pages/workflow_detail/components/header.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -437,10 +437,6 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
437437
revertUnsavedChanges();
438438
}}
439439
/>,
440-
<EuiText color="subdued" size="s">
441-
{`Last saved: ${workflowLastUpdated}`}
442-
</EuiText>,
443-
// TODO: placement may change for this
444440
<EuiSmallButtonEmpty
445441
disabled={false}
446442
onClick={() => {
@@ -449,6 +445,9 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
449445
>
450446
{`How it works`}
451447
</EuiSmallButtonEmpty>,
448+
<EuiText color="subdued" size="s">
449+
{`Last saved: ${workflowLastUpdated}`}
450+
</EuiText>,
452451
]}
453452
bottomBorder={false}
454453
rightSideGroupProps={{

public/pages/workflow_detail/resizable_workspace.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '@elastic/eui';
1515
import {
1616
CONFIG_STEP,
17+
CachedFormikState,
1718
INSPECTOR_TAB_ID,
1819
Workflow,
1920
WorkflowConfig,
@@ -46,6 +47,7 @@ interface ResizableWorkspaceProps {
4647
setSelectedStep: (step: CONFIG_STEP) => void;
4748
setUnsavedIngestProcessors: (unsavedIngestProcessors: boolean) => void;
4849
setUnsavedSearchProcessors: (unsavedSearchProcessors: boolean) => void;
50+
setCachedFormikState: (cachedFormikState: CachedFormikState) => void;
4951
}
5052

5153
const WORKFLOW_INPUTS_PANEL_ID = 'workflow_inputs_panel_id';
@@ -141,6 +143,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
141143
}
142144
setSelectedInspectorTabId(INSPECTOR_TAB_ID.QUERY);
143145
}}
146+
setCachedFormikState={props.setCachedFormikState}
144147
/>
145148
</EuiResizablePanel>
146149
<EuiResizableButton />

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

+6-16
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,14 @@ export function Query(props: QueryProps) {
6969
>(undefined);
7070

7171
// Standalone / sandboxed search request state. Users can test things out
72-
// without updating the base form / persisted value. We default to different values
73-
// based on the context (ingest or search), and update based on changes to the context
74-
// (ingest v. search), or if the parent form values are changed.
72+
// without updating the base form / persisted value.
73+
// Update if the parent form values are changed, or if a newly-created search pipeline is detected.
7574
const [tempRequest, setTempRequest] = useState<string>('');
7675
useEffect(() => {
77-
setTempRequest(
78-
props.selectedStep === CONFIG_STEP.INGEST
79-
? customStringify(FETCH_ALL_QUERY)
80-
: values?.search?.request || '{}'
81-
);
82-
}, [props.selectedStep]);
83-
useEffect(() => {
84-
if (
85-
!isEmpty(values?.search?.request) &&
86-
props.selectedStep === CONFIG_STEP.SEARCH
87-
) {
76+
if (!isEmpty(values?.search?.request)) {
8877
setTempRequest(values?.search?.request);
78+
} else {
79+
setTempRequest(customStringify(FETCH_ALL_QUERY));
8980
}
9081
}, [values?.search?.request]);
9182

@@ -170,8 +161,7 @@ export function Query(props: QueryProps) {
170161
singleSelection={{ asPlainText: true }}
171162
isClearable={false}
172163
options={
173-
props.hasSearchPipeline &&
174-
props.selectedStep === CONFIG_STEP.SEARCH
164+
props.hasSearchPipeline
175165
? SEARCH_OPTIONS
176166
: [SEARCH_OPTIONS[1]]
177167
}

public/pages/workflow_detail/workflow_detail.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
import { ResizableWorkspace } from './resizable_workspace';
3939
import {
4040
CONFIG_STEP,
41+
CachedFormikState,
4142
ERROR_GETTING_WORKFLOW_MSG,
4243
FETCH_ALL_QUERY_LARGE,
4344
MAX_WORKFLOW_NAME_TO_DISPLAY,
@@ -144,6 +145,13 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
144145
undefined
145146
);
146147

148+
// We persist some cached formik state we may want to save, even when the form is reset. For example,
149+
// when adding a processor, the form needs to be re-generated. But, we should persist any known
150+
// values that are touched or have errors.
151+
const [cachedFormikState, setCachedFormikState] = useState<
152+
CachedFormikState | undefined
153+
>(undefined);
154+
147155
// various form-related states. persisted here to pass down to the child's form and header components, particularly
148156
// to have consistency on the button states (enabled/disabled)
149157
const [isRunningIngest, setIsRunningIngest] = useState<boolean>(false);
@@ -199,6 +207,8 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
199207
<Formik
200208
enableReinitialize={true}
201209
initialValues={formValues}
210+
initialTouched={cachedFormikState?.touched}
211+
initialErrors={cachedFormikState?.errors}
202212
validationSchema={formSchema}
203213
onSubmit={(values) => {}}
204214
validate={(values) => {}}
@@ -233,6 +243,7 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
233243
setSelectedStep={setSelectedStep}
234244
setUnsavedIngestProcessors={setUnsavedIngestProcessors}
235245
setUnsavedSearchProcessors={setUnsavedSearchProcessors}
246+
setCachedFormikState={setCachedFormikState}
236247
/>
237248
</EuiPageBody>
238249
</EuiPage>

public/pages/workflow_detail/workflow_inputs/ingest_inputs/enrich_data.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66
import React from 'react';
77
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
88
import { ProcessorsList } from '../processors_list';
9-
import { PROCESSOR_CONTEXT, WorkflowConfig } from '../../../../../common';
9+
import {
10+
CachedFormikState,
11+
PROCESSOR_CONTEXT,
12+
WorkflowConfig,
13+
} from '../../../../../common';
1014
import { ProcessorsTitle } from '../../../../general_components';
1115
interface EnrichDataProps {
1216
uiConfig: WorkflowConfig;
1317
setUiConfig: (uiConfig: WorkflowConfig) => void;
18+
setCachedFormikState: (cachedFormikState: CachedFormikState) => void;
1419
}
1520

1621
/**
@@ -29,6 +34,7 @@ export function EnrichData(props: EnrichDataProps) {
2934
uiConfig={props.uiConfig}
3035
setUiConfig={props.setUiConfig}
3136
context={PROCESSOR_CONTEXT.INGEST}
37+
setCachedFormikState={props.setCachedFormikState}
3238
/>
3339
</EuiFlexItem>
3440
</EuiFlexGroup>

public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ export function IngestData(props: IngestDataProps) {
4040
<EuiText size="s">
4141
Invalid dimension detected for a vector field mapping. Ensure the
4242
dimension value is set correctly.{' '}
43-
<EuiLink href={KNN_VECTOR_DOCS_LINK}>Learn more</EuiLink>
43+
<EuiLink target="_blank" href={KNN_VECTOR_DOCS_LINK}>
44+
Learn more
45+
</EuiLink>
4446
</EuiText>
4547
}
4648
color="warning"

public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
88
import { SourceData } from './source_data';
99
import { EnrichData } from './enrich_data';
1010
import { IngestData } from './ingest_data';
11-
import { Workflow, WorkflowConfig } from '../../../../../common';
11+
import {
12+
CachedFormikState,
13+
Workflow,
14+
WorkflowConfig,
15+
} from '../../../../../common';
1216

1317
interface IngestInputsProps {
1418
setIngestDocs: (docs: string) => void;
1519
uiConfig: WorkflowConfig;
1620
setUiConfig: (uiConfig: WorkflowConfig) => void;
1721
workflow: Workflow | undefined;
1822
lastIngested: number | undefined;
23+
setCachedFormikState: (cachedFormikState: CachedFormikState) => void;
1924
}
2025

2126
/**
@@ -36,7 +41,11 @@ export function IngestInputs(props: IngestInputsProps) {
3641
<EuiHorizontalRule margin="none" />
3742
</EuiFlexItem>
3843
<EuiFlexItem grow={false}>
39-
<EnrichData uiConfig={props.uiConfig} setUiConfig={props.setUiConfig} />
44+
<EnrichData
45+
uiConfig={props.uiConfig}
46+
setUiConfig={props.setUiConfig}
47+
setCachedFormikState={props.setCachedFormikState}
48+
/>
4049
</EuiFlexItem>
4150
<EuiFlexItem grow={false}>
4251
<EuiHorizontalRule margin="none" />

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function TextField(props: TextFieldProps) {
6565
placeholder={props.placeholder || ''}
6666
value={field.value || getInitialValue('string')}
6767
onChange={(e) => {
68-
form.setFieldValue(props.fieldPath, e.target.value?.trim());
68+
form.setFieldValue(props.fieldPath, e.target.value);
6969
}}
7070
isInvalid={isInvalid}
7171
/>
@@ -76,7 +76,6 @@ export function TextField(props: TextFieldProps) {
7676
placeholder={props.placeholder || ''}
7777
value={field.value || getInitialValue('string')}
7878
onChange={(e) => {
79-
console.log('value: ', e.target.value);
8079
form.setFieldValue(props.fieldPath, e.target.value?.trim());
8180
}}
8281
isInvalid={isInvalid}

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

+30-3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) {
7777
const inputMapValue = getIn(values, inputMapFieldPath);
7878
const outputMapFieldPath = `${props.baseConfigPath}.${props.config.id}.output_map`;
7979
const outputMapValue = getIn(values, outputMapFieldPath);
80+
const [modelNotFound, setModelNotFound] = useState<boolean>(false);
8081

8182
// preview availability states
8283
// if there are preceding search request processors, we cannot fetch and display the interim transformed query.
@@ -139,18 +140,44 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) {
139140
setFieldTouched(outputMapFieldPath, false);
140141
}
141142

142-
// on initial load of the models, update model interface states
143+
// Listener to detect on changes that could affect the model details, such as
144+
// any defined model interface, or if the model is not found.
143145
useEffect(() => {
144146
if (!isEmpty(models)) {
145147
const modelId = getIn(values, modelIdFieldPath);
146-
if (modelId) {
148+
if (modelId && models[modelId]) {
147149
setModelInterface(models[modelId]?.interface);
150+
setModelNotFound(false);
151+
// Edge case: model ID found, but no matching deployed model found in the cluster.
152+
// If so, persist a model-not-found state, and touch all fields to trigger UI elements
153+
// indicating errors for this particular processor.
154+
} else if (modelId) {
155+
setModelNotFound(true);
156+
setFieldValue(modelIdFieldPath, '');
157+
try {
158+
const processorConfigPath = `${props.baseConfigPath}.${props.config.id}`;
159+
Object.keys(
160+
getIn(values, `${props.baseConfigPath}.${props.config.id}`)
161+
).forEach((modelFieldPath) => {
162+
setFieldTouched(`${processorConfigPath}.${modelFieldPath}`, true);
163+
});
164+
} catch (e) {}
148165
}
149166
}
150-
}, [models]);
167+
}, [models, getIn(values, modelIdFieldPath), props.uiConfig]);
151168

152169
return (
153170
<>
171+
{modelNotFound && (
172+
<>
173+
<EuiCallOut
174+
color="danger"
175+
size="s"
176+
title={<EuiText size="s">Model not found</EuiText>}
177+
/>
178+
<EuiSpacer size="s" />
179+
</>
180+
)}
154181
{isQueryModalOpen && (
155182
<OverrideQueryModal
156183
config={props.config}

0 commit comments

Comments
 (0)