From 5ccf18873fcacb1fd0924307025ccbe3182edbff Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen <ohltyler@amazon.com> Date: Thu, 13 Mar 2025 11:20:15 -0700 Subject: [PATCH 1/4] Make transform type dropdown consistent Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> --- .../processor_inputs/ml_processor_inputs/model_inputs.tsx | 1 + .../processor_inputs/ml_processor_inputs/model_outputs.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx index 49a2958d..580f72d6 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx @@ -346,6 +346,7 @@ export function ModelInputs(props: ModelInputsProps) { <EuiFlexItem grow={TYPE_FLEX_RATIO}> <EuiFlexItem> <EuiCompressedSuperSelect + fullWidth={true} disabled={false} options={INPUT_TRANSFORM_OPTIONS.map( (option) => diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx index e3d443e3..1762cede 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx @@ -211,6 +211,7 @@ export function ModelOutputs(props: ModelOutputsProps) { <EuiFlexItem grow={TYPE_FLEX_RATIO}> <EuiFlexItem> <EuiCompressedSuperSelect + fullWidth={true} disabled={false} options={OUTPUT_TRANSFORM_OPTIONS.map( (option) => From 2e69e0517d21b843707ca78a90081a4085ddb890 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen <ohltyler@amazon.com> Date: Thu, 13 Mar 2025 12:40:06 -0700 Subject: [PATCH 2/4] Get partially working for input map only Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> --- .../ml_processor_inputs/model_inputs.tsx | 80 +++++++++++++++++-- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx index 580f72d6..2f76d316 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx @@ -31,6 +31,7 @@ import { IndexMappings, InputMapEntry, InputMapFormValue, + Transform, TRANSFORM_TYPE, EMPTY_INPUT_MAP_ENTRY, WorkflowConfig, @@ -98,6 +99,16 @@ export function ModelInputs(props: ModelInputsProps) { ModelInterface | undefined >(undefined); + // get the model interface based on the selected ID and list of known models + useEffect(() => { + if (!isEmpty(models)) { + const modelId = getIn(values, modelFieldPath)?.id; + if (modelId) { + setModelInterface(models[modelId]?.interface); + } + } + }, [models, getIn(values, modelFieldPath)?.id]); + // various modal states const [templateModalIdx, setTemplateModalIdx] = useState<number | undefined>( undefined @@ -106,15 +117,70 @@ export function ModelInputs(props: ModelInputsProps) { number | undefined >(undefined); - // get the model interface based on the selected ID and list of known models + const [debouncedInputMap, setDebouncedInputMap] = useState<any>(); useEffect(() => { - if (!isEmpty(models)) { - const modelId = getIn(values, modelFieldPath)?.id; - if (modelId) { - setModelInterface(models[modelId]?.interface); - } + // Set a timeout to update debounced value after 500ms + const handler = setTimeout(() => { + setDebouncedInputMap(getIn(values, inputMapFieldPath)); + }, 500); + + // Cleanup the timeout if `query` changes before 500ms + return () => { + clearTimeout(handler); + }; + }, [getIn(values, inputMapFieldPath)]); + + // TODO: this works ok, but does not allow clearing out any form value, or it resets to the last-known/cached value. + // need to filter out on this use case. + + // Temporarily cache any configured transformations for different transform types. + // For example, if a user configures a prompt, swaps the transform + // type to "Data field", and swaps back to "Prompt", the prompt will be persisted. + const [inputMapCache, _] = useState<{ + [idx: number]: Transform[]; + }>({}); + useEffect(() => { + const curFormValues = debouncedInputMap as InputMapFormValue | undefined; + if (curFormValues !== undefined && !isEmpty(curFormValues)) { + // for each form value: populate the cache with a non-empty value and/or populate the + // form value with its cached value, if found. + curFormValues.forEach((mapEntry, idx) => { + const curCacheForIdx = inputMapCache[idx]; + if (curCacheForIdx === undefined || isEmpty(curCacheForIdx)) { + // case 1: there is no persisted state for this entry index. create a fresh arr + inputMapCache[idx] = [mapEntry.value]; + } else if ( + !curCacheForIdx.some( + (transform: Transform) => + transform.transformType === mapEntry.value.transformType + ) + ) { + // case 2: there is persisted state for this entry index, but not for the particular + // transform type. append to the arr + inputMapCache[idx] = [...inputMapCache[idx], mapEntry.value]; + } else { + // case 3: there is persisted state for this entry index, and for the particular transform type. + // Either update the cache with the current form value(s) (if non-empty), or update the form + // with any value found in the cache + inputMapCache[idx] = inputMapCache[idx].map((cachedEntry) => { + if (cachedEntry.transformType === mapEntry.value.transformType) { + const formValue = mapEntry.value.value; + // form is non-empty. update the cache + if (formValue !== undefined && !isEmpty(formValue)) { + return mapEntry.value; + // form is empty. update the form with cached value(s) + } else { + setFieldValue(`${inputMapFieldPath}.${idx}.value`, cachedEntry); + return cachedEntry; + } + } else { + return cachedEntry; + } + }); + } + }); } - }, [models, getIn(values, modelFieldPath)?.id]); + }, [debouncedInputMap]); // persisting doc/query/index mapping fields to collect a list // of options to display in the dropdowns when configuring input / output maps From ca75cb3ac3403aff875243c2ac2a864a2f24fa3f Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen <ohltyler@amazon.com> Date: Thu, 13 Mar 2025 17:01:02 -0700 Subject: [PATCH 3/4] Simplify to only trigger cache updates on transform type changes Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> --- .../ml_processor_inputs/model_inputs.tsx | 107 ++++++++---------- 1 file changed, 45 insertions(+), 62 deletions(-) diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx index 2f76d316..c1295d14 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx @@ -117,70 +117,12 @@ export function ModelInputs(props: ModelInputsProps) { number | undefined >(undefined); - const [debouncedInputMap, setDebouncedInputMap] = useState<any>(); - useEffect(() => { - // Set a timeout to update debounced value after 500ms - const handler = setTimeout(() => { - setDebouncedInputMap(getIn(values, inputMapFieldPath)); - }, 500); - - // Cleanup the timeout if `query` changes before 500ms - return () => { - clearTimeout(handler); - }; - }, [getIn(values, inputMapFieldPath)]); - - // TODO: this works ok, but does not allow clearing out any form value, or it resets to the last-known/cached value. - // need to filter out on this use case. - // Temporarily cache any configured transformations for different transform types. // For example, if a user configures a prompt, swaps the transform // type to "Data field", and swaps back to "Prompt", the prompt will be persisted. const [inputMapCache, _] = useState<{ [idx: number]: Transform[]; }>({}); - useEffect(() => { - const curFormValues = debouncedInputMap as InputMapFormValue | undefined; - if (curFormValues !== undefined && !isEmpty(curFormValues)) { - // for each form value: populate the cache with a non-empty value and/or populate the - // form value with its cached value, if found. - curFormValues.forEach((mapEntry, idx) => { - const curCacheForIdx = inputMapCache[idx]; - if (curCacheForIdx === undefined || isEmpty(curCacheForIdx)) { - // case 1: there is no persisted state for this entry index. create a fresh arr - inputMapCache[idx] = [mapEntry.value]; - } else if ( - !curCacheForIdx.some( - (transform: Transform) => - transform.transformType === mapEntry.value.transformType - ) - ) { - // case 2: there is persisted state for this entry index, but not for the particular - // transform type. append to the arr - inputMapCache[idx] = [...inputMapCache[idx], mapEntry.value]; - } else { - // case 3: there is persisted state for this entry index, and for the particular transform type. - // Either update the cache with the current form value(s) (if non-empty), or update the form - // with any value found in the cache - inputMapCache[idx] = inputMapCache[idx].map((cachedEntry) => { - if (cachedEntry.transformType === mapEntry.value.transformType) { - const formValue = mapEntry.value.value; - // form is non-empty. update the cache - if (formValue !== undefined && !isEmpty(formValue)) { - return mapEntry.value; - // form is empty. update the form with cached value(s) - } else { - setFieldValue(`${inputMapFieldPath}.${idx}.value`, cachedEntry); - return cachedEntry; - } - } else { - return cachedEntry; - } - }); - } - }); - } - }, [debouncedInputMap]); // persisting doc/query/index mapping fields to collect a list // of options to display in the dropdowns when configuring input / output maps @@ -448,19 +390,60 @@ export function ModelInputs(props: ModelInputsProps) { ) || '' } onChange={(option) => { + // before updating, cache any form values + const curCache = inputMapCache[idx]; + if ( + curCache === undefined || + isEmpty(curCache) + ) { + // case 1: there is no persisted state for this entry index. create a fresh arr + inputMapCache[idx] = [mapEntry.value]; + } else if ( + !curCache.some( + (transform: Transform) => + transform.transformType === + mapEntry.value.transformType + ) + ) { + // case 2: there is persisted state for this entry index, but not for the particular + // transform type. append to the arr + inputMapCache[idx] = [ + ...inputMapCache[idx], + mapEntry.value, + ]; + } else { + // case 3: there is persisted state for this entry index, and for the particular transform type. + // Update the cache with the current form value(s) + inputMapCache[idx] = inputMapCache[ + idx + ].map((cachedEntry) => { + if ( + cachedEntry.transformType === + mapEntry.value.transformType + ) { + return mapEntry.value; + } else { + return cachedEntry; + } + }); + } + setFieldValue( `${inputMapFieldPath}.${idx}.value.transformType`, option ); - // If the transform type changes, clear any set value and/or nested vars, - // as it will likely not make sense under other types/contexts. + // Pre-populate with any cached values, if found + const curCacheForOption = curCache?.find( + (transform: Transform) => + transform.transformType === option + ); setFieldValue( `${inputMapFieldPath}.${idx}.value.value`, - '' + curCacheForOption?.value || '' ); setFieldValue( `${inputMapFieldPath}.${idx}.value.nestedVars`, - [] + curCacheForOption?.nestedVars || [] ); }} /> From 3afb8549f75718fb349b8f6a2a0af00f11b2a014 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen <ohltyler@amazon.com> Date: Fri, 14 Mar 2025 09:41:03 -0700 Subject: [PATCH 4/4] Generalize to util fn; add for output map; Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> --- common/interfaces.ts | 4 ++ .../ml_processor_inputs/model_inputs.tsx | 53 +++++-------------- .../ml_processor_inputs/model_outputs.tsx | 28 ++++++++-- .../ml_processor_inputs/utils.ts | 46 ++++++++++++++++ 4 files changed, 86 insertions(+), 45 deletions(-) create mode 100644 public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/utils.ts diff --git a/common/interfaces.ts b/common/interfaces.ts index aa31c7d2..43fbfa15 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -126,6 +126,10 @@ export type OutputMapEntry = InputMapEntry; export type InputMapFormValue = InputMapEntry[]; export type OutputMapFormValue = OutputMapEntry[]; +export type MapCache = { + [idx: number]: Transform[]; +}; + export type InputMapArrayFormValue = InputMapFormValue[]; export type OutputMapArrayFormValue = OutputMapFormValue[]; diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx index c1295d14..5572a101 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx @@ -37,6 +37,7 @@ import { WorkflowConfig, getCharacterLimitedString, INPUT_TRANSFORM_OPTIONS, + MapCache, } from '../../../../../../common'; import { TextField, @@ -50,6 +51,7 @@ import { sanitizeJSONPath, } from '../../../../../utils'; import { ConfigureExpressionModal, ConfigureTemplateModal } from './modals/'; +import { updateCache } from './utils'; interface ModelInputsProps { config: IProcessorConfig; @@ -120,9 +122,7 @@ export function ModelInputs(props: ModelInputsProps) { // Temporarily cache any configured transformations for different transform types. // For example, if a user configures a prompt, swaps the transform // type to "Data field", and swaps back to "Prompt", the prompt will be persisted. - const [inputMapCache, _] = useState<{ - [idx: number]: Transform[]; - }>({}); + const [inputMapCache, setInputMapCache] = useState<MapCache>({}); // persisting doc/query/index mapping fields to collect a list // of options to display in the dropdowns when configuring input / output maps @@ -391,49 +391,19 @@ export function ModelInputs(props: ModelInputsProps) { } onChange={(option) => { // before updating, cache any form values - const curCache = inputMapCache[idx]; - if ( - curCache === undefined || - isEmpty(curCache) - ) { - // case 1: there is no persisted state for this entry index. create a fresh arr - inputMapCache[idx] = [mapEntry.value]; - } else if ( - !curCache.some( - (transform: Transform) => - transform.transformType === - mapEntry.value.transformType - ) - ) { - // case 2: there is persisted state for this entry index, but not for the particular - // transform type. append to the arr - inputMapCache[idx] = [ - ...inputMapCache[idx], - mapEntry.value, - ]; - } else { - // case 3: there is persisted state for this entry index, and for the particular transform type. - // Update the cache with the current form value(s) - inputMapCache[idx] = inputMapCache[ - idx - ].map((cachedEntry) => { - if ( - cachedEntry.transformType === - mapEntry.value.transformType - ) { - return mapEntry.value; - } else { - return cachedEntry; - } - }); - } - + const updatedCache = updateCache( + inputMapCache, + mapEntry, + idx + ); setFieldValue( `${inputMapFieldPath}.${idx}.value.transformType`, option ); // Pre-populate with any cached values, if found - const curCacheForOption = curCache?.find( + const curCacheForOption = updatedCache[ + idx + ]?.find( (transform: Transform) => transform.transformType === option ); @@ -445,6 +415,7 @@ export function ModelInputs(props: ModelInputsProps) { `${inputMapFieldPath}.${idx}.value.nestedVars`, curCacheForOption?.nestedVars || [] ); + setInputMapCache(updatedCache); }} /> </EuiFlexItem> diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx index 1762cede..87ce1b76 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx @@ -34,10 +34,13 @@ import { EMPTY_OUTPUT_MAP_ENTRY, ExpressionVar, OUTPUT_TRANSFORM_OPTIONS, + Transform, + MapCache, } from '../../../../../../common'; import { TextField } from '../../input_fields'; import { AppState } from '../../../../../store'; import { ConfigureMultiExpressionModal } from './modals'; +import { updateCache } from './utils'; interface ModelOutputsProps { config: IProcessorConfig; @@ -93,6 +96,11 @@ export function ModelOutputs(props: ModelOutputsProps) { } }, [models, getIn(values, modelFieldPath)?.id]); + // Temporarily cache any configured transformations for different transform types. + // For example, if a user configures a prompt, swaps the transform + // type to "Data field", and swaps back to "Prompt", the prompt will be persisted. + const [outputMapCache, setOutputMapCache] = useState<MapCache>({}); + // Adding a map entry to the end of the existing arr function addMapEntry(curEntries: OutputMapFormValue): void { const updatedEntries = [...curEntries, EMPTY_OUTPUT_MAP_ENTRY]; @@ -247,20 +255,32 @@ export function ModelOutputs(props: ModelOutputsProps) { ) || '' } onChange={(option) => { + // before updating, cache any form values + const updatedCache = updateCache( + outputMapCache, + mapEntry, + idx + ); setFieldValue( `${outputMapFieldPath}.${idx}.value.transformType`, option ); - // If the transform type changes, clear any set value and/or nested vars, - // as it will likely not make sense under other types/contexts. + // Pre-populate with any cached values, if found + const curCacheForOption = updatedCache[ + idx + ]?.find( + (transform: Transform) => + transform.transformType === option + ); setFieldValue( `${outputMapFieldPath}.${idx}.value.value`, - '' + curCacheForOption?.value || '' ); setFieldValue( `${outputMapFieldPath}.${idx}.value.nestedVars`, - [] + curCacheForOption?.nestedVars || [] ); + setOutputMapCache(updatedCache); }} /> </EuiFlexItem> diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/utils.ts b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/utils.ts new file mode 100644 index 00000000..358ea0d7 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/utils.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isEmpty } from 'lodash'; +import { + MapCache, + InputMapEntry, + OutputMapEntry, + Transform, +} from '../../../../../../common'; + +// Update a cache of data transform values based on a given form value +export function updateCache( + cache: MapCache, + mapEntry: InputMapEntry | OutputMapEntry, + idx: number // the mapEntry index +): MapCache { + const updatedCache = cache; + const curCache = updatedCache[idx]; + if (curCache === undefined || isEmpty(curCache)) { + // case 1: there is no persisted state for this entry index. create a fresh arr + updatedCache[idx] = [mapEntry.value]; + } else if ( + !curCache.some( + (transform: Transform) => + transform.transformType === mapEntry.value.transformType + ) + ) { + // case 2: there is persisted state for this entry index, but not for the particular + // transform type. append to the arr + updatedCache[idx] = [...updatedCache[idx], mapEntry.value]; + } else { + // case 3: there is persisted state for this entry index, and for the particular transform type. + // Update the cache with the current form value(s) + updatedCache[idx] = updatedCache[idx].map((cachedEntry) => { + if (cachedEntry.transformType === mapEntry.value.transformType) { + return mapEntry.value; + } else { + return cachedEntry; + } + }); + } + return updatedCache; +}