Skip to content

Commit 735f760

Browse files
authored
UX fit-n-finish updates VI (opensearch-project#570)
Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent 5ea50a0 commit 735f760

File tree

9 files changed

+170
-86
lines changed

9 files changed

+170
-86
lines changed

common/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ export const NORMALIZATION_PROCESSOR_LINK =
244244
export const GITHUB_FEEDBACK_LINK =
245245
'https://github.com/opensearch-project/dashboards-flow-framework/issues/new/choose';
246246
export const JSONPATH_DOCS_LINK = 'https://www.npmjs.com/package/jsonpath';
247+
export const KNN_VECTOR_DOCS_LINK =
248+
'https://opensearch.org/docs/latest/field-types/supported-field-types/knn-vector/';
247249

248250
/**
249251
* Text chunking algorithm constants

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

+19-2
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ import { AppState } from '../../../../store';
1919
import {
2020
getEmbeddingField,
2121
getEmbeddingModelDimensions,
22+
getFieldValue,
2223
getUpdatedIndexMappings,
2324
getUpdatedIndexSettings,
2425
isKnnIndex,
2526
removeVectorFieldFromIndexMappings,
2627
} from '../../../../utils';
2728

28-
interface AdvancedSettingsProps {}
29+
interface AdvancedSettingsProps {
30+
setHasInvalidDimensions: (hasInvalidDimensions: boolean) => void;
31+
}
2932

3033
/**
3134
* Input component for configuring ingest-side advanced settings
@@ -54,7 +57,10 @@ export function AdvancedSettings(props: AdvancedSettingsProps) {
5457
);
5558
if (processorModel?.connectorId !== undefined) {
5659
const processorConnector = connectors[processorModel?.connectorId];
57-
const dimension = getEmbeddingModelDimensions(processorConnector);
60+
const dimension =
61+
processorConnector !== undefined
62+
? getEmbeddingModelDimensions(processorConnector)
63+
: undefined;
5864

5965
// If a dimension is found, it is a known embedding model.
6066
// Ensure the index is configured to be knn-enabled.
@@ -115,6 +121,17 @@ export function AdvancedSettings(props: AdvancedSettingsProps) {
115121
}
116122
}, [getIn(values, 'ingest.enrich')]);
117123

124+
// listener to check if there is a dimension value set, and if so, check its validity
125+
useEffect(() => {
126+
try {
127+
const mappingsObj = JSON.parse(getIn(values, indexMappingsPath));
128+
const dimensionVal = getFieldValue(mappingsObj, 'dimension');
129+
props.setHasInvalidDimensions(
130+
dimensionVal !== undefined && typeof dimensionVal !== 'number'
131+
);
132+
} catch (e) {}
133+
}, [getIn(values, indexMappingsPath)]);
134+
118135
return (
119136
<EuiFlexGroup direction="column">
120137
<EuiFlexItem grow={false}>

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

+28-3
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,54 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React from 'react';
7-
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
6+
import React, { useState } from 'react';
7+
import {
8+
EuiCallOut,
9+
EuiFlexGroup,
10+
EuiFlexItem,
11+
EuiLink,
12+
EuiText,
13+
} from '@elastic/eui';
814
import { TextField } from '../input_fields';
915
import { AdvancedSettings } from './advanced_settings';
16+
import { KNN_VECTOR_DOCS_LINK } from '../../../../../common';
1017

1118
interface IngestDataProps {}
1219

1320
/**
1421
* Input component for configuring the data ingest (the OpenSearch index)
1522
*/
1623
export function IngestData(props: IngestDataProps) {
24+
const [hasInvalidDimensions, setHasInvalidDimensions] = useState<boolean>(
25+
false
26+
);
27+
1728
return (
1829
<EuiFlexGroup direction="column">
1930
<EuiFlexItem grow={false}>
2031
<EuiText size="s">
2132
<h3>Ingest data</h3>
2233
</EuiText>
2334
</EuiFlexItem>
35+
{hasInvalidDimensions && (
36+
<EuiCallOut
37+
style={{ marginLeft: '14px' }}
38+
size="s"
39+
title={
40+
<EuiText size="s">
41+
Invalid dimension detected for a vector field mapping. Ensure the
42+
dimension value is set correctly.{' '}
43+
<EuiLink href={KNN_VECTOR_DOCS_LINK}>Learn more</EuiLink>
44+
</EuiText>
45+
}
46+
color="warning"
47+
/>
48+
)}
2449
<EuiFlexItem>
2550
<TextField label="Index name" fieldPath={'ingest.index.name'} />
2651
</EuiFlexItem>
2752
<EuiFlexItem>
28-
<AdvancedSettings />
53+
<AdvancedSettings setHasInvalidDimensions={setHasInvalidDimensions} />
2954
</EuiFlexItem>
3055
</EuiFlexGroup>
3156
);

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

+5-63
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,20 @@ import {
2828
} from '@elastic/eui';
2929
import {
3030
IConfigField,
31-
IMAGE_FIELD_PATTERN,
3231
IProcessorConfig,
33-
LABEL_FIELD_PATTERN,
34-
MODEL_ID_PATTERN,
3532
ModelInterface,
3633
NO_TRANSFORMATION,
3734
OutputMapEntry,
38-
QUERY_IMAGE_PATTERN,
3935
QUERY_PRESETS,
40-
QUERY_TEXT_PATTERN,
4136
QueryPreset,
4237
RequestFormValues,
43-
TEXT_FIELD_PATTERN,
4438
TRANSFORM_TYPE,
45-
VECTOR_FIELD_PATTERN,
46-
VECTOR_PATTERN,
4739
WorkflowFormValues,
4840
} from '../../../../../../../common';
49-
import { parseModelOutputs } from '../../../../../../utils/utils';
41+
import {
42+
injectPlaceholdersInQueryString,
43+
parseModelOutputs,
44+
} from '../../../../../../utils/utils';
5045
import { JsonField } from '../../../input_fields';
5146
import { getFieldSchema, getInitialValue } from '../../../../../../utils';
5247
import '../../../../../../global-styles.scss';
@@ -193,60 +188,7 @@ export function OverrideQueryModal(props: OverrideQueryModalProps) {
193188
onClick: () => {
194189
formikProps.setFieldValue(
195190
'request',
196-
preset.query
197-
// sanitize the query preset string into valid template placeholder format, for
198-
// any placeholder values in the query.
199-
// for example, replacing `"{{vector}}"` with `${vector}`
200-
.replace(
201-
new RegExp(
202-
`"${VECTOR_FIELD_PATTERN}"`,
203-
'g'
204-
),
205-
`\$\{vector_field\}`
206-
)
207-
.replace(
208-
new RegExp(`"${VECTOR_PATTERN}"`, 'g'),
209-
`\$\{vector\}`
210-
)
211-
.replace(
212-
new RegExp(
213-
`"${TEXT_FIELD_PATTERN}"`,
214-
'g'
215-
),
216-
`\$\{text_field\}`
217-
)
218-
.replace(
219-
new RegExp(
220-
`"${IMAGE_FIELD_PATTERN}"`,
221-
'g'
222-
),
223-
`\$\{image_field\}`
224-
)
225-
.replace(
226-
new RegExp(
227-
`"${LABEL_FIELD_PATTERN}"`,
228-
'g'
229-
),
230-
`\$\{label_field\}`
231-
)
232-
.replace(
233-
new RegExp(
234-
`"${QUERY_TEXT_PATTERN}"`,
235-
'g'
236-
),
237-
`\$\{query_text\}`
238-
)
239-
.replace(
240-
new RegExp(
241-
`"${QUERY_IMAGE_PATTERN}"`,
242-
'g'
243-
),
244-
`\$\{query_image\}`
245-
)
246-
.replace(
247-
new RegExp(`"${MODEL_ID_PATTERN}"`, 'g'),
248-
`\$\{model_id\}`
249-
)
191+
injectPlaceholdersInQueryString(preset.query)
250192
);
251193
formikProps.setFieldTouched('request', true);
252194
setPresetsPopoverOpen(false);

public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
getInitialValue,
4343
getPlaceholdersFromQuery,
4444
injectParameters,
45+
injectPlaceholdersInQueryString,
4546
} from '../../../../utils';
4647
import { AppState, searchIndex, useAppDispatch } from '../../../../store';
4748
import { QueryParamsList, Results } from '../../../../general_components';
@@ -206,7 +207,9 @@ export function EditQueryModal(props: EditQueryModalProps) {
206207
onClick: () => {
207208
formikProps.setFieldValue(
208209
'request',
209-
preset.query
210+
injectPlaceholdersInQueryString(
211+
preset.query
212+
)
210213
);
211214
setPopoverOpen(false);
212215
},

public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx

+44-14
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,9 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
388388
props.setUnsavedIngestProcessors(false);
389389
props.setUnsavedSearchProcessors(false);
390390
await dispatch(
391+
// TODO: update to be synchronous provisioning, to prevent
392+
// having to wait/sleep before performing next actions.
393+
// https://github.com/opensearch-project/flow-framework/pull/1009
391394
provisionWorkflow({
392395
workflowId: updatedWorkflow.id as string,
393396
dataSourceId,
@@ -396,20 +399,47 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
396399
.unwrap()
397400
.then(async (result) => {
398401
await sleep(1000);
399-
success = true;
400-
// Kicking off an async task to re-fetch the workflow details
401-
// after some amount of time. Provisioning will finish in an indeterminate
402-
// amount of time and may be long and expensive; we add this single
403-
// auto-fetching to cover the majority of provisioning updates which
404-
// are inexpensive and will finish within milliseconds.
405-
setTimeout(async () => {
406-
dispatch(
407-
getWorkflow({
408-
workflowId: updatedWorkflow.id as string,
409-
dataSourceId,
410-
})
411-
);
412-
}, 1000);
402+
403+
await dispatch(
404+
getWorkflow({
405+
workflowId: updatedWorkflow.id as string,
406+
dataSourceId,
407+
})
408+
)
409+
.unwrap()
410+
.then(async (result: any) => {
411+
const resultWorkflow = result.workflow as Workflow;
412+
if (isEmpty(resultWorkflow.error)) {
413+
success = true;
414+
} else {
415+
success = false;
416+
getCore().notifications.toasts.addDanger(
417+
`Error creating all resources, rolling back: ${resultWorkflow.error}`
418+
);
419+
await dispatch(
420+
deprovisionWorkflow({
421+
apiBody: {
422+
workflowId: resultWorkflow?.id as string,
423+
resourceIds: getResourcesToBeForceDeleted(
424+
resultWorkflow
425+
),
426+
},
427+
dataSourceId,
428+
})
429+
)
430+
.unwrap()
431+
.then(async (result) => {
432+
setTimeout(async () => {
433+
await dispatch(
434+
getWorkflow({
435+
workflowId: updatedWorkflow.id as string,
436+
dataSourceId,
437+
})
438+
);
439+
}, 1000);
440+
});
441+
}
442+
});
413443
})
414444
.catch((error: any) => {
415445
console.error('Error provisioning updated workflow: ', error);

public/pages/workflows/new_workflow/quick_configure_inputs.tsx

+32-1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) {
6666
// Local field values state
6767
const [fieldValues, setFieldValues] = useState<QuickConfigureFields>({});
6868

69+
// Advanced config accordion state
70+
const [accordionState, setAccordionState] = useState<'open' | 'closed'>(
71+
'closed'
72+
);
73+
6974
// on initial load, and when there are any deployed models found, set
7075
// defaults for the field values for certain workflow types
7176
useEffect(() => {
@@ -117,13 +122,24 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) {
117122
}, [fieldValues]);
118123

119124
// Try to pre-fill the dimensions based on the chosen embedding model
125+
// If not found, we display a helper callout, and automatically
126+
// open the accordion to guide the user.
127+
const [unknownEmbeddingLength, setUnknownEmbeddingLength] = useState<boolean>(
128+
false
129+
);
120130
useEffect(() => {
121131
const selectedModel = deployedModels.find(
122132
(model) => model.id === fieldValues.embeddingModelId
123133
);
124134
if (selectedModel?.connectorId !== undefined) {
125135
const connector = connectors[selectedModel.connectorId];
126136
if (connector !== undefined) {
137+
const dimensions = getEmbeddingModelDimensions(connector);
138+
if (dimensions === undefined) {
139+
setUnknownEmbeddingLength(true);
140+
setAccordionState('open');
141+
}
142+
setUnknownEmbeddingLength(dimensions === undefined);
127143
setFieldValues({
128144
...fieldValues,
129145
embeddingLength: getEmbeddingModelDimensions(connector),
@@ -161,6 +177,16 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) {
161177
// we will always have a selectable embedding model.
162178
<>
163179
<EuiSpacer size="m" />
180+
{unknownEmbeddingLength && (
181+
<>
182+
<EuiCallOut
183+
size="s"
184+
title="No embedding length found. Make sure to manually configure below."
185+
color="warning"
186+
/>
187+
<EuiSpacer size="s" />
188+
</>
189+
)}
164190
<EuiCompressedFormRow
165191
fullWidth={true}
166192
label={
@@ -313,8 +339,13 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) {
313339
<EuiAccordion
314340
id="optionalConfiguration"
315341
buttonContent="Optional configuration"
316-
initialIsOpen={false}
342+
forceState={accordionState}
317343
data-testid="optionalConfigurationButton"
344+
onToggle={() =>
345+
accordionState === 'open'
346+
? setAccordionState('closed')
347+
: setAccordionState('open')
348+
}
318349
>
319350
<>
320351
<EuiSpacer size="s" />

public/pages/workflows/new_workflow/quick_configure_modal.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function QuickConfigureModal(props: QuickConfigureModalProps) {
117117
}, [models, quickConfigureFields?.llmId]);
118118

119119
return (
120-
<EuiModal onClose={() => props.onClose()} style={{ width: '30vw' }}>
120+
<EuiModal onClose={() => props.onClose()} style={{ width: '40vw' }}>
121121
<EuiModalHeader>
122122
<EuiModalHeaderTitle>
123123
<p>{`Quick configure`}</p>

0 commit comments

Comments
 (0)