From 90f332bc13d098ef7ba920bef666412b64a4a403 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 31 Oct 2024 16:49:20 -0700 Subject: [PATCH] Refactor subcomponents in advanced transform modals Signed-off-by: Tyler Ohlsen --- .../modals/input_transform_modal.tsx | 705 ++++++++++-------- .../modals/output_transform_modal.tsx | 672 +++++++++-------- 2 files changed, 741 insertions(+), 636 deletions(-) diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/input_transform_modal.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/input_transform_modal.tsx index da9f97be..1f3a61ed 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/input_transform_modal.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/input_transform_modal.tsx @@ -288,347 +288,401 @@ export function InputTransformModal(props: InputTransformModalProps) { setTempErrors(!isEmpty(formikProps.errors)); }, [formikProps.errors]); + const InputMap = ( + { + if (isEmpty(curArray)) { + setSelectedTransformOption(0); + } + }} + // If the map we are deleting is the one we last used to test, reset the state and + // default to the first map in the list. + onMapDelete={(idxToDelete) => { + if (selectedTransformOption === idxToDelete) { + setSelectedTransformOption(0); + setTransformedInput('{}'); + } + }} + addMapEntryButtonText="Add input" + addMapButtonText="(Advanced) Add input group" + /> + ); + + const OneToOneConfig = ( + + ); + + const FetchButton = ( + { + setIsFetching(true); + switch (props.context) { + case PROCESSOR_CONTEXT.INGEST: { + // get the current ingest pipeline up to, but not including, this processor + const curIngestPipeline = formikToPartialPipeline( + values, + props.uiConfig, + props.config.id, + false, + PROCESSOR_CONTEXT.INGEST + ); + // if there are preceding processors, we need to simulate the partial ingest pipeline, + // in order to get the latest transformed version of the docs + if (curIngestPipeline !== undefined) { + const curDocs = prepareDocsForSimulate( + values.ingest.docs, + values.ingest.index.name + ); + await dispatch( + simulatePipeline({ + apiBody: { + pipeline: curIngestPipeline as IngestPipelineConfig, + docs: [curDocs[0]], + }, + dataSourceId, + }) + ) + .unwrap() + .then((resp: SimulateIngestPipelineResponse) => { + const docObjs = unwrapTransformedDocs(resp); + if (docObjs.length > 0) { + setSourceInput(customStringify(docObjs[0])); + } + }) + .catch((error: any) => { + getCore().notifications.toasts.addDanger( + `Failed to fetch input data` + ); + }) + .finally(() => { + setIsFetching(false); + }); + } else { + try { + const docObjs = JSON.parse(values.ingest.docs) as {}[]; + if (docObjs.length > 0) { + setSourceInput(customStringify(docObjs[0])); + } + } catch { + } finally { + setIsFetching(false); + } + } + break; + } + case PROCESSOR_CONTEXT.SEARCH_REQUEST: { + // get the current search pipeline up to, but not including, this processor + const curSearchPipeline = formikToPartialPipeline( + values, + props.uiConfig, + props.config.id, + false, + PROCESSOR_CONTEXT.SEARCH_REQUEST + ); + // if there are preceding processors, we cannot generate. The button to render + // this modal should be disabled if the search pipeline would be enabled. We add + // this if check as an extra layer of checking, and if mechanism for gating + // this is changed in the future. + if (curSearchPipeline === undefined) { + setSourceInput(values.search.request); + } + setIsFetching(false); + break; + } + case PROCESSOR_CONTEXT.SEARCH_RESPONSE: { + // get the current search pipeline up to, but not including, this processor + const curSearchPipeline = formikToPartialPipeline( + values, + props.uiConfig, + props.config.id, + false, + PROCESSOR_CONTEXT.SEARCH_RESPONSE + ); + // Execute search. If there are preceding processors, augment the existing query with + // the partial search pipeline (inline) to get the latest transformed version of the response. + dispatch( + searchIndex({ + apiBody: { + index: values.search.index.name, + body: JSON.stringify({ + ...JSON.parse(values.search.request as string), + search_pipeline: curSearchPipeline || {}, + }), + }, + dataSourceId, + }) + ) + .unwrap() + .then(async (resp) => { + const hits = resp.hits.hits + .map((hit: SearchHit) => hit._source) + .slice(0, MAX_INPUT_DOCS); + if (hits.length > 0) { + setSourceInput( + // if one-to-one, treat the source input as a single retrieved document + // else, treat it as all of the returned documents + customStringify(tempOneToOne ? hits[0] : hits) + ); + } + }) + .catch((error: any) => { + getCore().notifications.toasts.addDanger( + `Failed to fetch source input data` + ); + }) + .finally(() => { + setIsFetching(false); + }); + break; + } + } + }} + > + Fetch data + + ); + + const SourceInput = ( + + ); + + const TransformedInput = ( + + ); + return ( - +

{`Configure input`}

- + <> - {(onIngestAndNoDocs || onSearchAndNoQuery) && ( - <> - - - - )} - {description} - - {props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE && ( - <> - + + +

Define transform

+
+
+ + - - - )} - Source input - { - setIsFetching(true); - switch (props.context) { - case PROCESSOR_CONTEXT.INGEST: { - // get the current ingest pipeline up to, but not including, this processor - const curIngestPipeline = formikToPartialPipeline( - values, - props.uiConfig, - props.config.id, - false, - PROCESSOR_CONTEXT.INGEST - ); - // if there are preceding processors, we need to simulate the partial ingest pipeline, - // in order to get the latest transformed version of the docs - if (curIngestPipeline !== undefined) { - const curDocs = prepareDocsForSimulate( - values.ingest.docs, - values.ingest.index.name - ); - await dispatch( - simulatePipeline({ - apiBody: { - pipeline: curIngestPipeline as IngestPipelineConfig, - docs: [curDocs[0]], - }, - dataSourceId, - }) - ) - .unwrap() - .then( - (resp: SimulateIngestPipelineResponse) => { - const docObjs = unwrapTransformedDocs(resp); - if (docObjs.length > 0) { - setSourceInput( - customStringify(docObjs[0]) - ); - } - } - ) - .catch((error: any) => { - getCore().notifications.toasts.addDanger( - `Failed to fetch input data` - ); - }) - .finally(() => { - setIsFetching(false); - }); - } else { - try { - const docObjs = JSON.parse( - values.ingest.docs - ) as {}[]; - if (docObjs.length > 0) { - setSourceInput(customStringify(docObjs[0])); - } - } catch { - } finally { - setIsFetching(false); - } - } - break; - } - case PROCESSOR_CONTEXT.SEARCH_REQUEST: { - // get the current search pipeline up to, but not including, this processor - const curSearchPipeline = formikToPartialPipeline( - values, - props.uiConfig, - props.config.id, - false, - PROCESSOR_CONTEXT.SEARCH_REQUEST - ); - // if there are preceding processors, we cannot generate. The button to render - // this modal should be disabled if the search pipeline would be enabled. We add - // this if check as an extra layer of checking, and if mechanism for gating - // this is changed in the future. - if (curSearchPipeline === undefined) { - setSourceInput(values.search.request); - } - setIsFetching(false); - break; - } - case PROCESSOR_CONTEXT.SEARCH_RESPONSE: { - // get the current search pipeline up to, but not including, this processor - const curSearchPipeline = formikToPartialPipeline( - values, - props.uiConfig, - props.config.id, - false, - PROCESSOR_CONTEXT.SEARCH_RESPONSE - ); - // Execute search. If there are preceding processors, augment the existing query with - // the partial search pipeline (inline) to get the latest transformed version of the response. - dispatch( - searchIndex({ - apiBody: { - index: values.search.index.name, - body: JSON.stringify({ - ...JSON.parse( - values.search.request as string - ), - search_pipeline: curSearchPipeline || {}, - }), - }, - dataSourceId, - }) - ) - .unwrap() - .then(async (resp) => { - const hits = resp.hits.hits - .map((hit: SearchHit) => hit._source) - .slice(0, MAX_INPUT_DOCS); - if (hits.length > 0) { - setSourceInput( - // if one-to-one, treat the source input as a single retrieved document - // else, treat it as all of the returned documents - customStringify( - tempOneToOne ? hits[0] : hits - ) - ); - } - }) - .catch((error: any) => { - getCore().notifications.toasts.addDanger( - `Failed to fetch source input data` - ); - }) - .finally(() => { - setIsFetching(false); - }); - break; - } - } - }} - > - Fetch - + +
- + {InputMap} - <> - Define transform - - { - if (isEmpty(curArray)) { - setSelectedTransformOption(0); - } - }} - // If the map we are deleting is the one we last used to test, reset the state and - // default to the first map in the list. - onMapDelete={(idxToDelete) => { - if (selectedTransformOption === idxToDelete) { - setSelectedTransformOption(0); - setTransformedInput('{}'); - } - }} - addMapEntryButtonText="Add input" - addMapButtonText="(Advanced) Add input group" - /> - - - - <> - - {isValid !== undefined && ( - - + + +

Preview

+
+
+ + + +
+ + <> + {(onIngestAndNoDocs || onSearchAndNoQuery) && ( + <> + - + )} - - {transformOptions.length <= 1 ? ( - Transformed input - ) : ( - Transformed input for - } - options={transformOptions} - value={selectedTransformOption} - onChange={(e) => { - setSelectedTransformOption( - Number(e.target.value) - ); - }} - /> - )} - - {!isEmpty(parseModelInputsObj(props.modelInterface)) && ( - - setPopoverOpen(false)} - panelPaddingSize="s" - button={ - setPopoverOpen(!popoverOpen)} - > - View input schema - - } - > - - The JSON Schema defining the model's expected - input - - - {customStringify( - parseModelInputsObj(props.modelInterface) - )} - - - + + {props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE && ( + <> + {OneToOneConfig} + + )} - - - - + {FetchButton} + +
+ + + + <> + + + Source input + + + + {SourceInput} + + + + <> + + {isValid !== undefined && ( + + + + )} + + {transformOptions.length <= 1 ? ( + Transformed input + ) : ( + + Transformed input for + + } + options={transformOptions} + value={selectedTransformOption} + onChange={(e) => { + setSelectedTransformOption( + Number(e.target.value) + ); + }} + /> + )} + + {!isEmpty( + parseModelInputsObj(props.modelInterface) + ) && ( + + setPopoverOpen(false)} + panelPaddingSize="s" + button={ + setPopoverOpen(!popoverOpen)} + > + View input schema + + } + > + + The JSON Schema defining the model's expected + input + + + {customStringify( + parseModelInputsObj(props.modelInterface) + )} + + + + )} + + + {TransformedInput} + + + {!isEmpty(originalPrompt) && ( @@ -648,10 +702,7 @@ export function InputTransformModal(props: InputTransformModalProps) { /> {isEmpty(JSON.parse(transformedInput)) && ( - + Transformed input is empty diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/output_transform_modal.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/output_transform_modal.tsx index d9070120..0a9bdde4 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/output_transform_modal.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/output_transform_modal.tsx @@ -26,6 +26,7 @@ import { EuiPopoverTitle, EuiCodeBlock, EuiCallOut, + EuiIconTip, } from '@elastic/eui'; import { IConfigField, @@ -209,17 +210,288 @@ export function OutputTransformModal(props: OutputTransformModalProps) { setTempErrors(!isEmpty(formikProps.errors)); }, [formikProps.errors]); + const OutputMap = ( + { + if (isEmpty(curArray)) { + setSelectedTransformOption(0); + } + }} + // If the map we are deleting is the one we last used to test, reset the state and + // default to the first map in the list. + onMapDelete={(idxToDelete) => { + if (selectedTransformOption === idxToDelete) { + setSelectedTransformOption(0); + setTransformedOutput('{}'); + } + }} + addMapEntryButtonText="Add output" + addMapButtonText="(Advanced) Add output group" + /> + ); + + const FullResponsePathConfig = ( + + ); + + const FetchButton = ( + { + setIsFetching(true); + switch (props.context) { + // note we skip search request processor context. that is because empty output maps are not supported. + // for more details, see comment in ml_processor_inputs.tsx + case PROCESSOR_CONTEXT.INGEST: { + // get the current ingest pipeline up to, and including this processor. + // remove any currently-configured output map since we only want the transformation + // up to, and including, the input map transformations + const valuesWithoutOutputMapConfig = cloneDeep(values); + set( + valuesWithoutOutputMapConfig, + props.outputMapFieldPath, + [] + ); + set( + valuesWithoutOutputMapConfig, + fullResponsePathPath, + getIn(formikProps.values, 'full_response_path') + ); + const curIngestPipeline = formikToPartialPipeline( + valuesWithoutOutputMapConfig, + props.uiConfig, + props.config.id, + true, + PROCESSOR_CONTEXT.INGEST + ) as IngestPipelineConfig; + const curDocs = prepareDocsForSimulate( + values.ingest.docs, + values.ingest.index.name + ); + await dispatch( + simulatePipeline({ + apiBody: { + pipeline: curIngestPipeline, + docs: [curDocs[0]], + }, + dataSourceId, + }) + ) + .unwrap() + .then((resp: SimulateIngestPipelineResponse) => { + try { + const docObjs = unwrapTransformedDocs(resp); + if (docObjs.length > 0) { + const sampleModelResult = + docObjs[0]?.inference_results || {}; + setSourceOutput(customStringify(sampleModelResult)); + } + } catch {} + }) + .catch((error: any) => { + getCore().notifications.toasts.addDanger( + `Failed to fetch input data` + ); + }) + .finally(() => { + setIsFetching(false); + }); + break; + } + case PROCESSOR_CONTEXT.SEARCH_RESPONSE: { + // get the current search pipeline up to, and including this processor. + // remove any currently-configured output map since we only want the transformation + // up to, and including, the input map transformations + const valuesWithoutOutputMapConfig = cloneDeep(values); + set( + valuesWithoutOutputMapConfig, + props.outputMapFieldPath, + [] + ); + set( + valuesWithoutOutputMapConfig, + fullResponsePathPath, + getIn(formikProps.values, 'full_response_path') + ); + const curSearchPipeline = formikToPartialPipeline( + valuesWithoutOutputMapConfig, + props.uiConfig, + props.config.id, + true, + PROCESSOR_CONTEXT.SEARCH_RESPONSE + ) as SearchPipelineConfig; + + // Execute search. Augment the existing query with + // the partial search pipeline (inline) to get the latest transformed + // version of the request. + dispatch( + searchIndex({ + apiBody: { + index: values.ingest.index.name, + body: JSON.stringify({ + ...JSON.parse(values.search.request as string), + search_pipeline: curSearchPipeline || {}, + }), + }, + dataSourceId, + }) + ) + .unwrap() + .then(async (resp) => { + const hits = resp.hits.hits.map( + (hit: SearchHit) => hit._source + ) as any[]; + if (hits.length > 0) { + const sampleModelResult = + hits[0].inference_results || {}; + setSourceOutput(customStringify(sampleModelResult)); + } + }) + .catch((error: any) => { + getCore().notifications.toasts.addDanger( + `Failed to fetch source output data` + ); + }) + .finally(() => { + setIsFetching(false); + }); + break; + } + } + }} + > + Fetch + + ); + + const SourceOutput = ( + + ); + + const TransformedOutput = ( + + ); + return ( - +

{`Configure output`}

- + <> + + + +

Define transform

+
+
+ + + +
+ + {OutputMap} + +
+ + + + +

Preview

+
+
+ + + +
+ {(onIngestAndNoDocs || onSearchAndNoQuery) && ( <> + {(props.context === PROCESSOR_CONTEXT.INGEST || + props.context === + PROCESSOR_CONTEXT.SEARCH_RESPONSE) && ( + <> + {FullResponsePathConfig} + + + )} + {FetchButton} )} - - Fetch some sample output data and see how it is - transformed. - - - {(props.context === PROCESSOR_CONTEXT.INGEST || - props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE) && ( + + + + <> - - - - )} - - - Source output - - {!isEmpty( - parseModelOutputsObj( - props.modelInterface, - tempFullResponsePath - ) - ) && ( - - setPopoverOpen(false)} - panelPaddingSize="s" - button={ - setPopoverOpen(!popoverOpen)} - > - View output schema - - } + + - - The JSON Schema defining the model's expected - output - - - {customStringify( - parseModelOutputsObj( - props.modelInterface, - tempFullResponsePath - ) - )} - - - - )} - - { - setIsFetching(true); - switch (props.context) { - // note we skip search request processor context. that is because empty output maps are not supported. - // for more details, see comment in ml_processor_inputs.tsx - case PROCESSOR_CONTEXT.INGEST: { - // get the current ingest pipeline up to, and including this processor. - // remove any currently-configured output map since we only want the transformation - // up to, and including, the input map transformations - const valuesWithoutOutputMapConfig = cloneDeep( - values - ); - set( - valuesWithoutOutputMapConfig, - props.outputMapFieldPath, - [] - ); - set( - valuesWithoutOutputMapConfig, - fullResponsePathPath, - getIn(formikProps.values, 'full_response_path') - ); - const curIngestPipeline = formikToPartialPipeline( - valuesWithoutOutputMapConfig, - props.uiConfig, - props.config.id, - true, - PROCESSOR_CONTEXT.INGEST - ) as IngestPipelineConfig; - const curDocs = prepareDocsForSimulate( - values.ingest.docs, - values.ingest.index.name - ); - await dispatch( - simulatePipeline({ - apiBody: { - pipeline: curIngestPipeline, - docs: [curDocs[0]], - }, - dataSourceId, - }) - ) - .unwrap() - .then((resp: SimulateIngestPipelineResponse) => { - try { - const docObjs = unwrapTransformedDocs(resp); - if (docObjs.length > 0) { - const sampleModelResult = - docObjs[0]?.inference_results || {}; - setSourceOutput( - customStringify(sampleModelResult) - ); + + Source output + + {!isEmpty( + parseModelOutputsObj( + props.modelInterface, + tempFullResponsePath + ) + ) && ( + + setPopoverOpen(false)} + panelPaddingSize="s" + button={ + + setPopoverOpen(!popoverOpen) + } + > + View output schema + } - } catch {} - }) - .catch((error: any) => { - getCore().notifications.toasts.addDanger( - `Failed to fetch input data` - ); - }) - .finally(() => { - setIsFetching(false); - }); - break; - } - case PROCESSOR_CONTEXT.SEARCH_RESPONSE: { - // get the current search pipeline up to, and including this processor. - // remove any currently-configured output map since we only want the transformation - // up to, and including, the input map transformations - const valuesWithoutOutputMapConfig = cloneDeep( - values - ); - set( - valuesWithoutOutputMapConfig, - props.outputMapFieldPath, - [] - ); - set( - valuesWithoutOutputMapConfig, - fullResponsePathPath, - getIn(formikProps.values, 'full_response_path') - ); - const curSearchPipeline = formikToPartialPipeline( - valuesWithoutOutputMapConfig, - props.uiConfig, - props.config.id, - true, - PROCESSOR_CONTEXT.SEARCH_RESPONSE - ) as SearchPipelineConfig; + > + + The JSON Schema defining the model's + expected output + + + {customStringify( + parseModelOutputsObj( + props.modelInterface, + tempFullResponsePath + ) + )} + + + + )} + + + {SourceOutput} +
+ +
+ + <> + {transformOptions.length <= 1 ? ( + Transformed output + ) : ( + Transformed output for + } + options={transformOptions} + value={selectedTransformOption} + onChange={(e) => { + setSelectedTransformOption( + Number(e.target.value) + ); + }} + /> + )} + + - // Execute search. Augment the existing query with - // the partial search pipeline (inline) to get the latest transformed - // version of the request. - dispatch( - searchIndex({ - apiBody: { - index: values.ingest.index.name, - body: JSON.stringify({ - ...JSON.parse( - values.search.request as string - ), - search_pipeline: curSearchPipeline || {}, - }), - }, - dataSourceId, - }) - ) - .unwrap() - .then(async (resp) => { - const hits = resp.hits.hits.map( - (hit: SearchHit) => hit._source - ) as any[]; - if (hits.length > 0) { - const sampleModelResult = - hits[0].inference_results || {}; - setSourceOutput( - customStringify(sampleModelResult) - ); - } - }) - .catch((error: any) => { - getCore().notifications.toasts.addDanger( - `Failed to fetch source output data` - ); - }) - .finally(() => { - setIsFetching(false); - }); - break; - } - } - }} - > - Fetch - - - - - - - <> - Define transform - - { - if (isEmpty(curArray)) { - setSelectedTransformOption(0); - } - }} - // If the map we are deleting is the one we last used to test, reset the state and - // default to the first map in the list. - onMapDelete={(idxToDelete) => { - if (selectedTransformOption === idxToDelete) { - setSelectedTransformOption(0); - setTransformedOutput('{}'); - } - }} - addMapEntryButtonText="Add output" - addMapButtonText="(Advanced) Add output group" - /> - - - - <> - {transformOptions.length <= 1 ? ( - Transformed output - ) : ( - Transformed output for - } - options={transformOptions} - value={selectedTransformOption} - onChange={(e) => { - setSelectedTransformOption(Number(e.target.value)); - }} - /> - )} - - - + {TransformedOutput} + + +