diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data_modal.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data_modal.tsx index ec700b7d..2f74402e 100644 --- a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data_modal.tsx +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data_modal.tsx @@ -22,6 +22,7 @@ import { EuiSmallFilterButton, EuiSuperSelectOption, EuiCompressedSuperSelect, + EuiSmallButtonEmpty, } from '@elastic/eui'; import { JsonField } from '../input_fields'; import { @@ -297,14 +298,13 @@ export function SourceDataModal(props: SourceDataProps) { > - onClose()} - fill={false} color="primary" data-testid="closeSourceDataButton" > Cancel - + onUpdate()} isLoading={isUpdating} @@ -313,7 +313,7 @@ export function SourceDataModal(props: SourceDataProps) { color="primary" data-testid="updateSourceDataButton" > - Update + Save 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..53d1ed47 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,405 @@ 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`} + {`Preview input transformation`} - + <> - {(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 - - - - > - - - <> - 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" - /> + {InputMap} > - <> - - {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} + > + + + + + <> + + + + Data before transformation + + + + + {SourceInput} + > + + + <> + + {isValid !== undefined && ( + + + + )} + + {transformOptions.length <= 1 ? ( + + Data after transformation + + ) : ( + + Data after transformation for + + } + options={transformOptions} + value={selectedTransformOption} + onChange={(e) => { + setSelectedTransformOption( + Number(e.target.value) + ); + }} + /> + )} + + {!isEmpty( + parseModelInputsObj(props.modelInterface) + ) && ( + + setPopoverOpen(false)} + panelPaddingSize="s" + button={ + setPopoverOpen(!popoverOpen)} + > + Input schema + + } + > + + The JSON Schema defining the model's expected + input + + + {customStringify( + parseModelInputsObj(props.modelInterface) + )} + + + + )} + + + {TransformedInput} + > + + {!isEmpty(originalPrompt) && ( @@ -648,10 +706,7 @@ export function InputTransformModal(props: InputTransformModalProps) { /> {isEmpty(JSON.parse(transformedInput)) && ( - + Transformed input is empty @@ -698,14 +753,13 @@ export function InputTransformModal(props: InputTransformModalProps) { - Cancel - + { // update the parent form values @@ -728,7 +782,7 @@ export function InputTransformModal(props: InputTransformModalProps) { color="primary" data-testid="updateInputTransformModalButton" > - Update + Save 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..50b5d37c 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,354 +210,413 @@ 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 data + + ); + + const SourceOutput = ( + + ); + + const TransformedOutput = ( + + ); + return ( - + - {`Configure output`} + {`Preview output transformation`} - + <> - {(onIngestAndNoDocs || onSearchAndNoQuery) && ( - <> - - - > - )} - - Fetch some sample output data and see how it is - transformed. - - - {(props.context === PROCESSOR_CONTEXT.INGEST || - props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE) && ( - <> - + + + Define transform + + + + - - > - )} - - - 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) - ); - } - } 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 - - - - > - - - <> - 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" - /> + {OutputMap} > - <> - {transformOptions.length <= 1 ? ( - Transformed output - ) : ( - Transformed output for + + + + Preview + + + + { - setSelectedTransformOption(Number(e.target.value)); - }} + position="right" /> - )} - - - > + + + + <> + {(onIngestAndNoDocs || onSearchAndNoQuery) && ( + <> + + + > + )} + {(props.context === PROCESSOR_CONTEXT.INGEST || + props.context === + PROCESSOR_CONTEXT.SEARCH_RESPONSE) && ( + <> + {FullResponsePathConfig} + + > + )} + {FetchButton} + > + + + + + <> + + + + + Data before transformation + + + {!isEmpty( + parseModelOutputsObj( + props.modelInterface, + tempFullResponsePath + ) + ) && ( + + setPopoverOpen(false)} + panelPaddingSize="s" + button={ + + setPopoverOpen(!popoverOpen) + } + > + Output schema + + } + > + + The JSON Schema defining the model's + expected output + + + {customStringify( + parseModelOutputsObj( + props.modelInterface, + tempFullResponsePath + ) + )} + + + + )} + + + {SourceOutput} + + > + + + <> + {transformOptions.length <= 1 ? ( + Data after transformation + ) : ( + + Data after transformation for + + } + options={transformOptions} + value={selectedTransformOption} + onChange={(e) => { + setSelectedTransformOption( + Number(e.target.value) + ); + }} + /> + )} + + + + {TransformedOutput} + > + + - Cancel - + { // update the parent form values @@ -578,7 +638,7 @@ export function OutputTransformModal(props: OutputTransformModalProps) { color="primary" data-testid="updateOutputTransformModalButton" > - Update + Save diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx index eca28d3c..444f8c97 100644 --- a/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx +++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx @@ -17,6 +17,7 @@ import { EuiModalHeaderTitle, EuiPopover, EuiSpacer, + EuiSmallButtonEmpty, } from '@elastic/eui'; import { JsonField } from '../input_fields'; import { @@ -139,14 +140,13 @@ export function EditQueryModal(props: EditQueryModalProps) { /> - props.setModalOpen(false)} - fill={false} color="primary" data-testid="cancelSearchQueryButton" > Cancel - + { setFieldValue(props.queryFieldPath, tempRequest); @@ -158,7 +158,7 @@ export function EditQueryModal(props: EditQueryModalProps) { color="primary" data-testid="updateSearchQueryButton" > - Update + Save
{`Configure input`}
{`Preview input transformation`}
{`Configure output`}
{`Preview output transformation`}