diff --git a/common/interfaces.ts b/common/interfaces.ts
index c445a4f0..baa08165 100644
--- a/common/interfaces.ts
+++ b/common/interfaces.ts
@@ -122,6 +122,20 @@ export type RequestFormValues = {
};
export type RequestSchema = WorkflowSchema;
+// Form / schema interfaces for the input transform sub-form
+export type InputTransformFormValues = {
+ input_map: MapArrayFormValue;
+ one_to_one: ConfigFieldValue;
+};
+export type InputTransformSchema = WorkflowSchema;
+
+// Form / schema interfaces for the output transform sub-form
+export type OutputTransformFormValues = {
+ output_map: MapArrayFormValue;
+ full_response_path: ConfigFieldValue;
+};
+export type OutputTransformSchema = WorkflowSchema;
+
/**
********** WORKSPACE TYPES/INTERFACES **********
*/
diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/map_array_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/map_array_field.tsx
index 5daf11f2..196f0077 100644
--- a/public/pages/workflow_detail/workflow_inputs/input_fields/map_array_field.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/input_fields/map_array_field.tsx
@@ -18,7 +18,6 @@ import {
import { Field, FieldProps, getIn, useFormikContext } from 'formik';
import {
EMPTY_MAP_ENTRY,
- IConfigField,
MapArrayFormValue,
MapEntry,
WorkflowFormValues,
@@ -26,7 +25,6 @@ import {
import { MapField } from './map_field';
interface MapArrayFieldProps {
- field: IConfigField;
fieldPath: string; // the full path in string-form to the field (e.g., 'ingest.enrich.processors.text_embedding_processor.inputField')
helpText?: string;
keyTitle?: string;
diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs.tsx
index 39c7f649..20a6c936 100644
--- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs.tsx
@@ -25,7 +25,6 @@ import {
PROCESSOR_CONTEXT,
WorkflowConfig,
JSONPATH_ROOT_SELECTOR,
- ML_INFERENCE_DOCS_LINK,
WorkflowFormValues,
ModelInterface,
IndexMappings,
@@ -75,15 +74,9 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) {
) as IConfigField;
const modelFieldPath = `${props.baseConfigPath}.${props.config.id}.${modelField.id}`;
const modelIdFieldPath = `${modelFieldPath}.id`;
- const inputMapField = props.config.fields.find(
- (field) => field.id === 'input_map'
- ) as IConfigField;
- const inputMapFieldPath = `${props.baseConfigPath}.${props.config.id}.${inputMapField.id}`;
+ const inputMapFieldPath = `${props.baseConfigPath}.${props.config.id}.input_map`;
const inputMapValue = getIn(values, inputMapFieldPath);
- const outputMapField = props.config.fields.find(
- (field) => field.id === 'output_map'
- ) as IConfigField;
- const outputMapFieldPath = `${props.baseConfigPath}.${props.config.id}.${outputMapField.id}`;
+ const outputMapFieldPath = `${props.baseConfigPath}.${props.config.id}.output_map`;
const outputMapValue = getIn(values, outputMapFieldPath);
const fullResponsePath = getIn(
values,
@@ -250,7 +243,6 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) {
config={props.config}
baseConfigPath={props.baseConfigPath}
context={props.context}
- inputMapField={inputMapField}
inputMapFieldPath={inputMapFieldPath}
modelInterface={modelInterface}
valueOptions={
@@ -269,7 +261,6 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) {
config={props.config}
baseConfigPath={props.baseConfigPath}
context={props.context}
- outputMapField={outputMapField}
outputMapFieldPath={outputMapFieldPath}
modelInterface={modelInterface}
onClose={() => setIsOutputTransformModalOpen(false)}
@@ -365,7 +356,6 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) {
();
+ const { values, setFieldValue, setFieldTouched } = useFormikContext<
+ WorkflowFormValues
+ >();
+
+ // sub-form values/schema
+ const inputTransformFormValues = {
+ input_map: getInitialValue('mapArray'),
+ one_to_one: getInitialValue('boolean'),
+ } as InputTransformFormValues;
+ const inputTransformFormSchema = yup.object({
+ input_map: getFieldSchema({
+ type: 'mapArray',
+ } as IConfigField),
+ one_to_one: getFieldSchema(
+ {
+ type: 'boolean',
+ } as IConfigField,
+ true
+ ),
+ }) as InputTransformSchema;
+
+ // persist standalone values. update / initialize when it is first opened
+ const [tempErrors, setTempErrors] = useState(false);
+ const [tempOneToOne, setTempOneToOne] = useState(false);
+ const [tempInputMap, setTempInputMap] = useState([]);
// various prompt states
const [viewPromptDetails, setViewPromptDetails] = useState(false);
@@ -103,9 +131,7 @@ export function InputTransformModal(props: InputTransformModalProps) {
const [transformedInput, setTransformedInput] = useState('{}');
// get some current form values
- const map = getIn(values, props.inputMapFieldPath) as MapArrayFormValue;
const oneToOnePath = `${props.baseConfigPath}.${props.config.id}.one_to_one`;
- const oneToOne = getIn(values, oneToOnePath);
const docs = getIn(values, 'ingest.docs');
let docObjs = [] as {}[] | undefined;
try {
@@ -124,13 +150,13 @@ export function InputTransformModal(props: InputTransformModalProps) {
isEmpty(queryObj);
// selected transform state
- const transformOptions = map.map((_, idx) => ({
+ const transformOptions = tempInputMap.map((_, idx) => ({
value: idx,
text: `Prediction ${idx + 1}`,
})) as EuiSelectOption[];
const [selectedTransformOption, setSelectedTransformOption] = useState<
- number | undefined
- >((transformOptions[0]?.value as number) ?? undefined);
+ number
+ >((transformOptions[0]?.value as number) ?? 0);
// popover state containing the model interface details, if applicable
const [popoverOpen, setPopoverOpen] = useState(false);
@@ -148,27 +174,23 @@ export function InputTransformModal(props: InputTransformModalProps) {
// hook to re-generate the transform when any inputs to the transform are updated
useEffect(() => {
- if (
- !isEmpty(map) &&
- !isEmpty(JSON.parse(sourceInput)) &&
- selectedTransformOption !== undefined
- ) {
+ if (!isEmpty(tempInputMap) && !isEmpty(JSON.parse(sourceInput))) {
let sampleSourceInput = {} as {} | [];
try {
sampleSourceInput = JSON.parse(sourceInput);
const output =
// Edge case: users are collapsing input docs into a single input field when many-to-one is selected
// fo input transforms on search response processors.
- oneToOne === false &&
+ tempOneToOne === false &&
props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE &&
Array.isArray(sampleSourceInput)
? generateArrayTransform(
sampleSourceInput as [],
- map[selectedTransformOption]
+ tempInputMap[selectedTransformOption]
)
: generateTransform(
sampleSourceInput,
- map[selectedTransformOption]
+ tempInputMap[selectedTransformOption]
);
setTransformedInput(customStringify(output));
@@ -176,7 +198,7 @@ export function InputTransformModal(props: InputTransformModalProps) {
} else {
setTransformedInput('{}');
}
- }, [map, sourceInput, selectedTransformOption]);
+ }, [tempInputMap, sourceInput, selectedTransformOption]);
// hook to re-determine validity when the generated output changes
// utilize Ajv JSON schema validator library. For more info/examples, see
@@ -231,384 +253,370 @@ export function InputTransformModal(props: InputTransformModalProps) {
// hook to clear the source input when one_to_one is toggled
useEffect(() => {
setSourceInput('{}');
- }, [oneToOne]);
+ }, [tempOneToOne]);
return (
-
-
-
- {`Configure input`}
-
-
-
-
-
- <>
- {(onIngestAndNoDocs || onSearchAndNoQuery) && (
- <>
-
-
- >
- )}
- {description}
-
- {props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE && (
- <>
-
-
- >
- )}
- 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]));
+ {}}
+ validate={(values) => {}}
+ >
+ {(formikProps) => {
+ // override to parent form values when changes detected
+ useEffect(() => {
+ formikProps.setFieldValue(
+ 'input_map',
+ getIn(values, props.inputMapFieldPath)
+ );
+ }, [getIn(values, props.inputMapFieldPath)]);
+ useEffect(() => {
+ formikProps.setFieldValue('one_to_one', getIn(values, oneToOnePath));
+ }, [getIn(values, oneToOnePath)]);
+
+ // update temp vars when form changes are detected
+ useEffect(() => {
+ setTempInputMap(getIn(formikProps.values, 'input_map'));
+ }, [getIn(formikProps.values, 'input_map')]);
+ useEffect(() => {
+ setTempOneToOne(getIn(formikProps.values, 'one_to_one'));
+ }, [getIn(formikProps.values, 'one_to_one')]);
+
+ // update tempErrors if errors detected
+ useEffect(() => {
+ setTempErrors(!isEmpty(formikProps.errors));
+ }, [formikProps.errors]);
+
+ return (
+
+
+
+ {`Configure input`}
+
+
+
+
+
+ <>
+ {(onIngestAndNoDocs || onSearchAndNoQuery) && (
+ <>
+
+
+ >
+ )}
+ {description}
+
+ {props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE && (
+ <>
+
+
+ >
+ )}
+ 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);
+ }
}
- })
- .catch((error: any) => {
- getCore().notifications.toasts.addDanger(
- `Failed to fetch input data`
+ 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
);
- })
- .finally(() => {
+ // 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);
- });
- } else {
- try {
- const docObjs = JSON.parse(
- values.ingest.docs
- ) as {}[];
- if (docObjs.length > 0) {
- setSourceInput(customStringify(docObjs[0]));
+ break;
}
- } 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(oneToOne ? hits[0] : hits)
+ 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;
}
- })
- .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"
- />
- >
-
-
- <>
-
- {isValid !== undefined && (
-
-
+ Fetch
+
+
+
-
- )}
-
- {transformOptions.length <= 1 ? (
- Transformed input
- ) : (
- Transformed input for
+ >
+
+
+ <>
+ Define transform
+
+ {
- setSelectedTransformOption(Number(e.target.value));
+ valueOptions={props.valueOptions}
+ // If the map we are adding is the first one, populate the selected option to index 0
+ onMapAdd={(curArray) => {
+ 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"
/>
- )}
+ >
- {!isEmpty(parseModelInputsObj(props.modelInterface)) && (
-
- setPopoverOpen(false)}
- panelPaddingSize="s"
- button={
- setPopoverOpen(!popoverOpen)}
+
+ <>
+
+ {isValid !== undefined && (
+
- View input schema
-
- }
- >
-
- The JSON Schema defining the model's expected input
-
-
- {customStringify(
- parseModelInputsObj(props.modelInterface)
+
+
+ )}
+
+ {transformOptions.length <= 1 ? (
+ Transformed input
+ ) : (
+ Transformed input for
+ }
+ options={transformOptions}
+ value={selectedTransformOption}
+ onChange={(e) => {
+ setSelectedTransformOption(
+ Number(e.target.value)
+ );
+ }}
+ />
)}
-
-
-
- )}
-
-
-
- >
-
- {!isEmpty(originalPrompt) && (
-
- <>
-
-
- Transformed prompt
-
-
- setViewPromptDetails(!viewPromptDetails)}
- disabled={isEmpty(JSON.parse(transformedInput))}
- />
-
- {isEmpty(JSON.parse(transformedInput)) && (
-
-
- Transformed input is empty
-
-
- )}
-
- {viewPromptDetails && (
- <>
+
+ {!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)
+ )}
+
+
+
+ )}
+
-
- setViewTransformedPrompt(!viewTransformedPrompt)
- }
- />
-
>
+
+ {!isEmpty(originalPrompt) && (
+
+ <>
+
+
+ Transformed prompt
+
+
+
+ setViewPromptDetails(!viewPromptDetails)
+ }
+ disabled={isEmpty(JSON.parse(transformedInput))}
+ />
+
+ {isEmpty(JSON.parse(transformedInput)) && (
+
+
+ Transformed input is empty
+
+
+ )}
+
+ {viewPromptDetails && (
+ <>
+
+
+ setViewTransformedPrompt(!viewTransformedPrompt)
+ }
+ />
+
+
+ >
+ )}
+ >
+
)}
- >
-
- )}
-
-
-
-
- Close
-
-
-
+
+
+
+
+ Cancel
+
+ {
+ // update the parent form values
+ if (props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE) {
+ setFieldValue(
+ oneToOnePath,
+ getIn(formikProps.values, 'one_to_one')
+ );
+ setFieldTouched(oneToOnePath, true);
+ }
+ setFieldValue(
+ props.inputMapFieldPath,
+ getIn(formikProps.values, 'input_map')
+ );
+ setFieldTouched(props.inputMapFieldPath, true);
+ props.onClose();
+ }}
+ isDisabled={tempErrors} // blocking update until valid input is given
+ fill={true}
+ color="primary"
+ data-testid="updateInputTransformModalButton"
+ >
+ Update
+
+
+
+ );
+ }}
+
);
}
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 ab7aaf5c..d9070120 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
@@ -4,8 +4,9 @@
*/
import React, { useState, useEffect } from 'react';
-import { useFormikContext, getIn } from 'formik';
+import { useFormikContext, getIn, Formik } from 'formik';
import { cloneDeep, isEmpty, set } from 'lodash';
+import * as yup from 'yup';
import {
EuiCodeEditor,
EuiFlexGroup,
@@ -35,6 +36,8 @@ import {
ML_INFERENCE_RESPONSE_DOCS_LINK,
MapArrayFormValue,
ModelInterface,
+ OutputTransformFormValues,
+ OutputTransformSchema,
PROCESSOR_CONTEXT,
SearchHit,
SearchPipelineConfig,
@@ -46,6 +49,8 @@ import {
import {
formikToPartialPipeline,
generateTransform,
+ getFieldSchema,
+ getInitialValue,
prepareDocsForSimulate,
unwrapTransformedDocs,
} from '../../../../../utils';
@@ -67,7 +72,6 @@ interface OutputTransformModalProps {
config: IProcessorConfig;
baseConfigPath: string;
context: PROCESSOR_CONTEXT;
- outputMapField: IConfigField;
outputMapFieldPath: string;
modelInterface: ModelInterface | undefined;
onClose: () => void;
@@ -79,7 +83,33 @@ interface OutputTransformModalProps {
export function OutputTransformModal(props: OutputTransformModalProps) {
const dispatch = useAppDispatch();
const dataSourceId = getDataSourceId();
- const { values } = useFormikContext();
+ const { values, setFieldValue, setFieldTouched } = useFormikContext<
+ WorkflowFormValues
+ >();
+
+ // sub-form values/schema
+ const outputTransformFormValues = {
+ output_map: getInitialValue('mapArray'),
+ full_response_path: getInitialValue('boolean'),
+ } as OutputTransformFormValues;
+ const outputTransformFormSchema = yup.object({
+ output_map: getFieldSchema({
+ type: 'mapArray',
+ } as IConfigField),
+ full_response_path: getFieldSchema(
+ {
+ type: 'boolean',
+ } as IConfigField,
+ true
+ ),
+ }) as OutputTransformSchema;
+
+ // persist standalone values. update / initialize when it is first opened
+ const [tempErrors, setTempErrors] = useState(false);
+ const [tempFullResponsePath, setTempFullResponsePath] = useState(
+ false
+ );
+ const [tempOutputMap, setTempOutputMap] = useState([]);
// fetching input data state
const [isFetching, setIsFetching] = useState(false);
@@ -89,9 +119,7 @@ export function OutputTransformModal(props: OutputTransformModalProps) {
const [transformedOutput, setTransformedOutput] = useState('{}');
// get some current form values
- const map = getIn(values, props.outputMapFieldPath) as MapArrayFormValue;
const fullResponsePathPath = `${props.baseConfigPath}.${props.config.id}.full_response_path`;
- const fullResponsePath = getIn(values, fullResponsePathPath);
const docs = getIn(values, 'ingest.docs');
let docObjs = [] as {}[] | undefined;
try {
@@ -113,360 +141,449 @@ export function OutputTransformModal(props: OutputTransformModalProps) {
const [popoverOpen, setPopoverOpen] = useState(false);
// selected transform state
- const transformOptions = map.map((_, idx) => ({
+ const transformOptions = tempOutputMap.map((_, idx) => ({
value: idx,
text: `Prediction ${idx + 1}`,
})) as EuiSelectOption[];
const [selectedTransformOption, setSelectedTransformOption] = useState<
- number | undefined
- >((transformOptions[0]?.value as number) ?? undefined);
+ number
+ >((transformOptions[0]?.value as number) ?? 0);
// hook to re-generate the transform when any inputs to the transform are updated
useEffect(() => {
- if (
- !isEmpty(map) &&
- !isEmpty(JSON.parse(sourceOutput)) &&
- selectedTransformOption !== undefined
- ) {
+ if (!isEmpty(tempOutputMap) && !isEmpty(JSON.parse(sourceOutput))) {
let sampleSourceOutput = {};
try {
sampleSourceOutput = JSON.parse(sourceOutput);
const output = generateTransform(
sampleSourceOutput,
- map[selectedTransformOption]
+ tempOutputMap[selectedTransformOption]
);
setTransformedOutput(customStringify(output));
} catch {}
} else {
setTransformedOutput('{}');
}
- }, [map, sourceOutput, selectedTransformOption]);
+ }, [tempOutputMap, sourceOutput, selectedTransformOption]);
// hook to clear the source output when full_response_path is toggled
useEffect(() => {
setSourceOutput('{}');
- }, [fullResponsePath]);
+ }, [tempFullResponsePath]);
return (
-
-
-
- {`Configure output`}
-
-
-
-
-
- <>
- {(onIngestAndNoDocs || onSearchAndNoQuery) && (
- <>
-
-
- >
- )}
-
- Fetch some sample output data and see how it is transformed.
-
-
- {(props.context === PROCESSOR_CONTEXT.INGEST ||
- props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE) && (
- <>
-
-
- >
- )}
-
+ {}}
+ validate={(values) => {}}
+ >
+ {(formikProps) => {
+ // override to parent form values when changes detected
+ useEffect(() => {
+ formikProps.setFieldValue(
+ 'output_map',
+ getIn(values, props.outputMapFieldPath)
+ );
+ }, [getIn(values, props.outputMapFieldPath)]);
+ useEffect(() => {
+ formikProps.setFieldValue(
+ 'full_response_path',
+ getIn(values, fullResponsePathPath)
+ );
+ }, [getIn(values, fullResponsePathPath)]);
+
+ // update temp vars when form changes are detected
+ useEffect(() => {
+ setTempOutputMap(getIn(formikProps.values, 'output_map'));
+ }, [getIn(formikProps.values, 'output_map')]);
+ useEffect(() => {
+ setTempFullResponsePath(
+ getIn(formikProps.values, 'full_response_path')
+ );
+ }, [getIn(formikProps.values, 'full_response_path')]);
+
+ // update tempErrors if errors detected
+ useEffect(() => {
+ setTempErrors(!isEmpty(formikProps.errors));
+ }, [formikProps.errors]);
+
+ return (
+
+
+
+ {`Configure output`}
+
+
+
+
- Source output
+ <>
+ {(onIngestAndNoDocs || onSearchAndNoQuery) && (
+ <>
+
+
+ >
+ )}
+
+ 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)
+ );
+ }
+ } 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
+
+
+
+ >
- {!isEmpty(
- parseModelOutputsObj(props.modelInterface, fullResponsePath)
- ) && (
-
- setPopoverOpen(false)}
- panelPaddingSize="s"
- button={
- setPopoverOpen(!popoverOpen)}
- >
- View output schema
-
+
+ <>
+ Define transform
+
+
-
- The JSON Schema defining the model's expected output
-
-
- {customStringify(
- parseModelOutputsObj(
- props.modelInterface,
- fullResponsePath
- )
- )}
-
-
-
- )}
+ keyPlaceholder={
+ props.context === PROCESSOR_CONTEXT.SEARCH_REQUEST
+ ? 'Specify a query field'
+ : 'Define a document field'
+ }
+ valueTitle="Name"
+ valuePlaceholder="Name"
+ valueOptions={
+ tempFullResponsePath
+ ? undefined
+ : parseModelOutputs(props.modelInterface, false)
+ }
+ // If the map we are adding is the first one, populate the selected option to index 0
+ onMapAdd={(curArray) => {
+ 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));
+ }}
+ />
+ )}
+
+
+ >
+
+
+
{
- 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,
- `ingest.enrich.${props.config.id}.output_map`,
- []
- );
- 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,
- `search.enrichResponse.${props.config.id}.output_map`,
- []
- );
- const curSearchPipeline = formikToPartialPipeline(
- valuesWithoutOutputMapConfig,
- props.uiConfig,
- props.config.id,
- true,
- PROCESSOR_CONTEXT.SEARCH_RESPONSE
- ) as SearchPipelineConfig;
+ onClick={props.onClose}
+ fill={false}
+ color="primary"
+ data-testid="cancelOutputTransformModalButton"
+ >
+ Cancel
+
+ {
+ // update the parent form values
+ setFieldValue(
+ fullResponsePathPath,
+ getIn(formikProps.values, 'full_response_path')
+ );
+ setFieldTouched(fullResponsePathPath, true);
- // 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;
- }
- }
+ setFieldValue(
+ props.outputMapFieldPath,
+ getIn(formikProps.values, 'output_map')
+ );
+ setFieldTouched(props.outputMapFieldPath, true);
+ props.onClose();
}}
+ isDisabled={tempErrors} // blocking update until valid input is given
+ fill={true}
+ color="primary"
+ data-testid="updateOutputTransformModalButton"
>
- Fetch
+ Update
-
-
- >
-
-
- <>
- 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));
- }}
- />
- )}
-
-
- >
-
-
-
-
-
- Close
-
-
-
+
+
+ );
+ }}
+
);
}