From cafdd5660c9209b572360f47176dde1e3e0a34af Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 29 Oct 2024 10:45:23 -0700 Subject: [PATCH 1/5] Refactor modal to standalone component Signed-off-by: Tyler Ohlsen --- common/constants.ts | 5 + .../ingest_inputs/source_data.tsx | 147 ++--------------- .../ingest_inputs/source_data_modal.tsx | 155 ++++++++++++++++++ 3 files changed, 170 insertions(+), 137 deletions(-) create mode 100644 public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data_modal.tsx diff --git a/common/constants.ts b/common/constants.ts index 03c9e175..5e8062de 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -484,3 +484,8 @@ export enum CONFIG_STEP { INGEST = 'Ingestion pipeline', SEARCH = 'Search pipeline', } +export enum SOURCE_OPTIONS { + MANUAL = 'manual', + UPLOAD = 'upload', + EXISTING_INDEX = 'existing_index', +} diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx index c905ddec..85031145 100644 --- a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx @@ -4,32 +4,21 @@ */ import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; import { getIn, useFormikContext } from 'formik'; import { EuiSmallButton, - EuiCompressedFilePicker, EuiFlexGroup, EuiFlexItem, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, EuiSpacer, EuiText, - EuiFilterGroup, - EuiSmallFilterButton, - EuiSuperSelectOption, - EuiCompressedSuperSelect, EuiCodeBlock, EuiSmallButtonEmpty, } from '@elastic/eui'; -import { JsonField } from '../input_fields'; import { FETCH_ALL_QUERY, IndexMappings, MapEntry, + SOURCE_OPTIONS, SearchHit, Workflow, WorkflowConfig, @@ -38,13 +27,9 @@ import { isVectorSearchUseCase, toFormattedDate, } from '../../../../../common'; -import { - AppState, - getMappings, - searchIndex, - useAppDispatch, -} from '../../../../store'; +import { getMappings, searchIndex, useAppDispatch } from '../../../../store'; import { getDataSourceId } from '../../../../utils'; +import { SourceDataModal } from './source_data_modal'; interface SourceDataProps { workflow: Workflow | undefined; @@ -53,12 +38,6 @@ interface SourceDataProps { lastIngested: number | undefined; } -enum SOURCE_OPTIONS { - MANUAL = 'manual', - UPLOAD = 'upload', - EXISTING_INDEX = 'existing_index', -} - /** * Input component for configuring the source data for ingest. */ @@ -66,7 +45,6 @@ export function SourceData(props: SourceDataProps) { const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); const { values, setFieldValue } = useFormikContext(); - const indices = useSelector((state: AppState) => state.opensearch.indices); // empty/populated docs state let docs = []; @@ -83,14 +61,6 @@ export function SourceData(props: SourceDataProps) { // edit modal state const [isEditModalOpen, setIsEditModalOpen] = useState(false); - // files state. when a file is read, update the form value. - const fileReader = new FileReader(); - fileReader.onload = (e) => { - if (e.target) { - setFieldValue('ingest.docs', e.target.result); - } - }; - // selected index state. when an index is selected, update several form values (if vector search) const [selectedIndex, setSelectedIndex] = useState( undefined @@ -188,110 +158,13 @@ export function SourceData(props: SourceDataProps) { return ( <> {isEditModalOpen && ( - setIsEditModalOpen(false)} - style={{ width: '70vw' }} - > - - -

{`Import data`}

-
-
- - <> - - setSelectedOption(SOURCE_OPTIONS.MANUAL)} - data-testid="manualEditSourceDataButton" - > - Manual - - setSelectedOption(SOURCE_OPTIONS.UPLOAD)} - data-testid="uploadSourceDataButton" - > - Upload - - - setSelectedOption(SOURCE_OPTIONS.EXISTING_INDEX) - } - data-testid="selectIndexSourceDataButton" - > - Existing index - - - - {selectedOption === SOURCE_OPTIONS.UPLOAD && ( - <> - { - if (files && files.length > 0) { - fileReader.readAsText(files[0]); - } - }} - display="default" - /> - - - )} - {selectedOption === SOURCE_OPTIONS.EXISTING_INDEX && ( - <> - - Up to 5 sample documents will be automatically populated. - - - - ({ - value: option.name, - inputDisplay: ( - {option.name} - ), - disabled: false, - } as EuiSuperSelectOption) - )} - valueOfSelected={selectedIndex} - onChange={(option) => { - setSelectedIndex(option); - }} - isInvalid={false} - /> - - - )} - - - - - setIsEditModalOpen(false)} - fill={false} - color="primary" - data-testid="closeSourceDataButton" - > - Close - - -
+ )} 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 new file mode 100644 index 00000000..7948892b --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data_modal.tsx @@ -0,0 +1,155 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { useSelector } from 'react-redux'; +import { useFormikContext } from 'formik'; +import { + EuiSmallButton, + EuiCompressedFilePicker, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, + EuiFilterGroup, + EuiSmallFilterButton, + EuiSuperSelectOption, + EuiCompressedSuperSelect, +} from '@elastic/eui'; +import { JsonField } from '../input_fields'; +import { SOURCE_OPTIONS, WorkspaceFormValues } from '../../../../../common'; +import { AppState } from '../../../../store'; + +interface SourceDataProps { + selectedOption: SOURCE_OPTIONS; + setSelectedOption: (option: SOURCE_OPTIONS) => void; + selectedIndex: string | undefined; + setSelectedIndex: (index: string) => void; + setIsModalOpen: (isOpen: boolean) => void; +} + +/** + * Modal for configuring the source data for ingest. + */ +export function SourceDataModal(props: SourceDataProps) { + const { setFieldValue } = useFormikContext(); + const indices = useSelector((state: AppState) => state.opensearch.indices); + + // files state. when a file is read, update the form value. + const fileReader = new FileReader(); + fileReader.onload = (e) => { + if (e.target) { + setFieldValue('ingest.docs', e.target.result); + } + }; + + return ( + props.setIsModalOpen(false)} + style={{ width: '70vw' }} + > + + +

{`Import data`}

+
+
+ + <> + + props.setSelectedOption(SOURCE_OPTIONS.MANUAL)} + data-testid="manualEditSourceDataButton" + > + Manual + + props.setSelectedOption(SOURCE_OPTIONS.UPLOAD)} + data-testid="uploadSourceDataButton" + > + Upload + + + props.setSelectedOption(SOURCE_OPTIONS.EXISTING_INDEX) + } + data-testid="selectIndexSourceDataButton" + > + Existing index + + + + {props.selectedOption === SOURCE_OPTIONS.UPLOAD && ( + <> + { + if (files && files.length > 0) { + fileReader.readAsText(files[0]); + } + }} + display="default" + /> + + + )} + {props.selectedOption === SOURCE_OPTIONS.EXISTING_INDEX && ( + <> + + Up to 5 sample documents will be automatically populated. + + + + ({ + value: option.name, + inputDisplay: {option.name}, + disabled: false, + } as EuiSuperSelectOption) + )} + valueOfSelected={props.selectedIndex} + onChange={(option) => { + props.setSelectedIndex(option); + }} + isInvalid={false} + /> + + + )} + + + + + props.setIsModalOpen(false)} + fill={false} + color="primary" + data-testid="closeSourceDataButton" + > + Close + + +
+ ); +} From f17cb53e4ce095caa666f37159648051d06a461c Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 29 Oct 2024 11:57:55 -0700 Subject: [PATCH 2/5] Add interim state besides existing index option Signed-off-by: Tyler Ohlsen --- common/interfaces.ts | 10 +- .../ingest_inputs/source_data.tsx | 6 +- .../ingest_inputs/source_data_modal.tsx | 290 +++++++++++------- .../input_fields/json_field.tsx | 4 +- .../input_fields/model_field.tsx | 4 +- .../input_fields/number_field.tsx | 4 +- .../input_fields/select_field.tsx | 4 +- .../select_with_custom_options.tsx | 4 +- .../input_fields/text_field.tsx | 4 +- public/utils/config_to_schema_utils.ts | 2 +- 10 files changed, 202 insertions(+), 130 deletions(-) diff --git a/common/interfaces.ts b/common/interfaces.ts index b7eb7781..6f13aa71 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -67,9 +67,6 @@ export type IndexConfig = { settings: IConfigField; }; -// TODO: may expand to just IndexConfig (including mappings/settings info) -// if we want to persist this for users using some existing index, -// and want to pass that index config around. export type SearchIndexConfig = { name: IConfigField; }; @@ -113,6 +110,13 @@ export type WorkflowSchemaObj = { }; export type WorkflowSchema = ObjectSchema; +// Form / schema interfaces for the ingest docs sub-form +export type IngestDocsFormValues = { + docs: FormikValues; +}; +export type IngestDocsSchemaObj = WorkflowSchemaObj; +export type IngestDocsSchema = WorkflowSchema; + /** ********** WORKSPACE TYPES/INTERFACES ********** */ diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx index 85031145..76cdc92e 100644 --- a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx @@ -22,7 +22,7 @@ import { SearchHit, Workflow, WorkflowConfig, - WorkspaceFormValues, + WorkflowFormValues, customStringify, isVectorSearchUseCase, toFormattedDate, @@ -44,7 +44,7 @@ interface SourceDataProps { export function SourceData(props: SourceDataProps) { const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); - const { values, setFieldValue } = useFormikContext(); + const { values, setFieldValue } = useFormikContext(); // empty/populated docs state let docs = []; @@ -236,7 +236,7 @@ export function SourceData(props: SourceDataProps) { // only be executed for workflows coming from preset vector search use cases. function getProcessorInfo( uiConfig: WorkflowConfig, - values: WorkspaceFormValues + values: WorkflowFormValues ): { processorId: string | undefined; inputMapEntry: MapEntry | undefined; 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 7948892b..32287bed 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 @@ -3,9 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { useFormikContext } from 'formik'; +import { Formik, getIn, useFormikContext } from 'formik'; +import * as yup from 'yup'; import { EuiSmallButton, EuiCompressedFilePicker, @@ -22,8 +23,14 @@ import { EuiCompressedSuperSelect, } from '@elastic/eui'; import { JsonField } from '../input_fields'; -import { SOURCE_OPTIONS, WorkspaceFormValues } from '../../../../../common'; +import { + IConfigField, + IngestDocsFormValues, + SOURCE_OPTIONS, + WorkflowFormValues, +} from '../../../../../common'; import { AppState } from '../../../../store'; +import { getFieldSchema, getInitialValue } from '../../../../utils'; interface SourceDataProps { selectedOption: SOURCE_OPTIONS; @@ -37,119 +44,180 @@ interface SourceDataProps { * Modal for configuring the source data for ingest. */ export function SourceDataModal(props: SourceDataProps) { - const { setFieldValue } = useFormikContext(); + const { values, setFieldValue } = useFormikContext(); const indices = useSelector((state: AppState) => state.opensearch.indices); - // files state. when a file is read, update the form value. - const fileReader = new FileReader(); - fileReader.onload = (e) => { - if (e.target) { - setFieldValue('ingest.docs', e.target.result); - } - }; + // sub-form values/schema + const docsFormValues = { + docs: getInitialValue('jsonArray'), + } as IngestDocsFormValues; + const docsFormSchema = yup.object({ + docs: getFieldSchema({ + type: 'jsonArray', + } as IConfigField), + }); + + // persist standalone values. update when there is changes detected to the parent form + const [tempDocs, setTempDocs] = useState('[]'); + useEffect(() => { + setTempDocs(getIn(values, 'ingest.docs')); + }, [getIn(values, 'ingest.docs')]); + + function onClose() { + props.setIsModalOpen(false); + } + + function onUpdate() { + // 1. Update the form with the temp docs + setFieldValue('ingest.docs', tempDocs); + + props.setIsModalOpen(false); + } return ( - props.setIsModalOpen(false)} - style={{ width: '70vw' }} + {}} + validate={(values) => {}} > - - -

{`Import data`}

-
-
- - <> - - props.setSelectedOption(SOURCE_OPTIONS.MANUAL)} - data-testid="manualEditSourceDataButton" - > - Manual - - props.setSelectedOption(SOURCE_OPTIONS.UPLOAD)} - data-testid="uploadSourceDataButton" - > - Upload - - - props.setSelectedOption(SOURCE_OPTIONS.EXISTING_INDEX) - } - data-testid="selectIndexSourceDataButton" - > - Existing index - - - - {props.selectedOption === SOURCE_OPTIONS.UPLOAD && ( - <> - { - if (files && files.length > 0) { - fileReader.readAsText(files[0]); - } - }} - display="default" - /> - - - )} - {props.selectedOption === SOURCE_OPTIONS.EXISTING_INDEX && ( - <> - - Up to 5 sample documents will be automatically populated. - - - - ({ - value: option.name, - inputDisplay: {option.name}, - disabled: false, - } as EuiSuperSelectOption) + {(formikProps) => { + // internal hook to loop back and update tempDocs when form changes are detected + useEffect(() => { + setTempDocs(getIn(formikProps.values, 'docs')); + }, [getIn(formikProps.values, 'docs')]); + + return ( + onClose()} style={{ width: '70vw' }}> + + +

{`Import data`}

+
+
+ + <> + + + props.setSelectedOption(SOURCE_OPTIONS.MANUAL) + } + data-testid="manualEditSourceDataButton" + > + Manual + + + props.setSelectedOption(SOURCE_OPTIONS.UPLOAD) + } + data-testid="uploadSourceDataButton" + > + Upload + + + props.setSelectedOption(SOURCE_OPTIONS.EXISTING_INDEX) + } + data-testid="selectIndexSourceDataButton" + > + Existing index + + + + {props.selectedOption === SOURCE_OPTIONS.UPLOAD && ( + <> + { + if (files && files.length > 0) { + // create a custom filereader to update form with file values + const fileReader = new FileReader(); + fileReader.onload = (e) => { + if (e.target) { + formikProps.setFieldValue( + 'docs', + e.target.result as string + ); + } + }; + fileReader.readAsText(files[0]); + } + }} + display="default" + /> + + + )} + {props.selectedOption === SOURCE_OPTIONS.EXISTING_INDEX && ( + <> + + Up to 5 sample documents will be automatically populated. + + + + ({ + value: option.name, + inputDisplay: ( + {option.name} + ), + disabled: false, + } as EuiSuperSelectOption) + )} + valueOfSelected={props.selectedIndex} + onChange={(option) => { + props.setSelectedIndex(option); + }} + isInvalid={false} + /> + + )} - valueOfSelected={props.selectedIndex} - onChange={(option) => { - props.setSelectedIndex(option); - }} - isInvalid={false} - /> - - - )} - - - - - props.setIsModalOpen(false)} - fill={false} - color="primary" - data-testid="closeSourceDataButton" - > - Close - - -
+ + +
+ + onClose()} + fill={false} + color="primary" + data-testid="cancelSourceDataButton" + > + Cancel + + onUpdate()} + fill={true} + color="primary" + data-testid="updateSourceDataButton" + > + Update + + +
+ ); + }} + ); } diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/json_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/json_field.tsx index ca49bd97..88c61e8d 100644 --- a/public/pages/workflow_detail/workflow_inputs/input_fields/json_field.tsx +++ b/public/pages/workflow_detail/workflow_inputs/input_fields/json_field.tsx @@ -11,7 +11,7 @@ import { EuiLink, EuiText, } from '@elastic/eui'; -import { WorkspaceFormValues, customStringify } from '../../../../../common'; +import { WorkflowFormValues, customStringify } from '../../../../../common'; import { camelCaseToTitleString } from '../../../../utils'; interface JsonFieldProps { @@ -31,7 +31,7 @@ interface JsonFieldProps { export function JsonField(props: JsonFieldProps) { const validate = props.validate !== undefined ? props.validate : true; - const { errors, touched, values } = useFormikContext(); + const { errors, touched, values } = useFormikContext(); // temp input state. only format when users click out of the code editor const [jsonStr, setJsonStr] = useState('{}'); diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx index b87ca877..cb8ec809 100644 --- a/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx +++ b/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { MODEL_STATE, - WorkspaceFormValues, + WorkflowFormValues, ModelFormValue, IConfigField, ML_CHOOSE_MODEL_LINK, @@ -46,7 +46,7 @@ export function ModelField(props: ModelFieldProps) { // keeps re-rendering this component (and subsequently re-fetching data) as they're building flows const models = useSelector((state: AppState) => state.ml.models); - const { errors, touched, values } = useFormikContext(); + const { errors, touched, values } = useFormikContext(); // Deployed models state const [deployedModels, setDeployedModels] = useState([]); diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/number_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/number_field.tsx index d847dfd9..c628c5de 100644 --- a/public/pages/workflow_detail/workflow_inputs/input_fields/number_field.tsx +++ b/public/pages/workflow_detail/workflow_inputs/input_fields/number_field.tsx @@ -11,7 +11,7 @@ import { EuiText, EuiCompressedFieldNumber, } from '@elastic/eui'; -import { WorkspaceFormValues } from '../../../../../common'; +import { WorkflowFormValues } from '../../../../../common'; import { camelCaseToTitleString, getInitialValue } from '../../../../utils'; interface NumberFieldProps { @@ -27,7 +27,7 @@ interface NumberFieldProps { * An input field for a component where users input numbers */ export function NumberField(props: NumberFieldProps) { - const { errors, touched } = useFormikContext(); + const { errors, touched } = useFormikContext(); return ( diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/select_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/select_field.tsx index 3146a944..e27f9d43 100644 --- a/public/pages/workflow_detail/workflow_inputs/input_fields/select_field.tsx +++ b/public/pages/workflow_detail/workflow_inputs/input_fields/select_field.tsx @@ -11,7 +11,7 @@ import { EuiSuperSelectOption, EuiText, } from '@elastic/eui'; -import { WorkspaceFormValues, IConfigField } from '../../../../../common'; +import { WorkflowFormValues, IConfigField } from '../../../../../common'; import { camelCaseToTitleString } from '../../../../utils'; interface SelectFieldProps { @@ -24,7 +24,7 @@ interface SelectFieldProps { * A generic select field from a list of preconfigured options */ export function SelectField(props: SelectFieldProps) { - const { errors, touched } = useFormikContext(); + const { errors, touched } = useFormikContext(); return ( diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/select_with_custom_options.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/select_with_custom_options.tsx index ac698805..6f0067ec 100644 --- a/public/pages/workflow_detail/workflow_inputs/input_fields/select_with_custom_options.tsx +++ b/public/pages/workflow_detail/workflow_inputs/input_fields/select_with_custom_options.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useState } from 'react'; import { getIn, useFormikContext } from 'formik'; import { get, isEmpty } from 'lodash'; import { EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { WorkspaceFormValues } from '../../../../../common'; +import { WorkflowFormValues } from '../../../../../common'; interface SelectWithCustomOptionsProps { fieldPath: string; @@ -20,7 +20,7 @@ interface SelectWithCustomOptionsProps { */ export function SelectWithCustomOptions(props: SelectWithCustomOptionsProps) { const { values, setFieldTouched, setFieldValue } = useFormikContext< - WorkspaceFormValues + WorkflowFormValues >(); // selected option state diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/text_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/text_field.tsx index e8194663..0dd0b5a3 100644 --- a/public/pages/workflow_detail/workflow_inputs/input_fields/text_field.tsx +++ b/public/pages/workflow_detail/workflow_inputs/input_fields/text_field.tsx @@ -11,7 +11,7 @@ import { EuiLink, EuiText, } from '@elastic/eui'; -import { WorkspaceFormValues } from '../../../../../common'; +import { WorkflowFormValues } from '../../../../../common'; import { getInitialValue } from '../../../../utils'; interface TextFieldProps { @@ -28,7 +28,7 @@ interface TextFieldProps { * An input field for a component where users input plaintext */ export function TextField(props: TextFieldProps) { - const { errors, touched } = useFormikContext(); + const { errors, touched } = useFormikContext(); return ( {({ field, form }: FieldProps) => { diff --git a/public/utils/config_to_schema_utils.ts b/public/utils/config_to_schema_utils.ts index b441cab7..dd5a23d2 100644 --- a/public/utils/config_to_schema_utils.ts +++ b/public/utils/config_to_schema_utils.ts @@ -103,7 +103,7 @@ function processorsConfigToSchema(processorsConfig: ProcessorsConfig): Schema { **************** Yup (validation) utils ********************** */ -function getFieldSchema( +export function getFieldSchema( field: IConfigField, optional: boolean = false ): Schema { From 68793eace464b9f92bcee821360656d166ae4ef6 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 29 Oct 2024 15:46:34 -0700 Subject: [PATCH 3/5] Support existing indices option, move remaining state Signed-off-by: Tyler Ohlsen --- .../ingest_inputs/source_data.tsx | 74 +----------- .../ingest_inputs/source_data_modal.tsx | 108 ++++++++++++++++-- 2 files changed, 101 insertions(+), 81 deletions(-) diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx index 76cdc92e..b5a0e735 100644 --- a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/source_data.tsx @@ -15,20 +15,14 @@ import { EuiSmallButtonEmpty, } from '@elastic/eui'; import { - FETCH_ALL_QUERY, - IndexMappings, MapEntry, SOURCE_OPTIONS, - SearchHit, Workflow, WorkflowConfig, WorkflowFormValues, - customStringify, isVectorSearchUseCase, toFormattedDate, } from '../../../../../common'; -import { getMappings, searchIndex, useAppDispatch } from '../../../../store'; -import { getDataSourceId } from '../../../../utils'; import { SourceDataModal } from './source_data_modal'; interface SourceDataProps { @@ -42,8 +36,6 @@ interface SourceDataProps { * Input component for configuring the source data for ingest. */ export function SourceData(props: SourceDataProps) { - const dispatch = useAppDispatch(); - const dataSourceId = getDataSourceId(); const { values, setFieldValue } = useFormikContext(); // empty/populated docs state @@ -61,66 +53,6 @@ export function SourceData(props: SourceDataProps) { // edit modal state const [isEditModalOpen, setIsEditModalOpen] = useState(false); - // selected index state. when an index is selected, update several form values (if vector search) - const [selectedIndex, setSelectedIndex] = useState( - undefined - ); - useEffect(() => { - if (selectedIndex !== undefined) { - // 1. fetch and set sample docs - dispatch( - searchIndex({ - apiBody: { - index: selectedIndex, - body: FETCH_ALL_QUERY, - searchPipeline: '_none', - }, - dataSourceId, - }) - ) - .unwrap() - .then((resp) => { - const docObjs = resp.hits?.hits - ?.slice(0, 5) - ?.map((hit: SearchHit) => hit?._source); - setFieldValue('ingest.docs', customStringify(docObjs)); - }); - - // 2. fetch index mappings, and try to set defaults for the ML processor configs, if applicable - if (isVectorSearchUseCase(props.workflow)) { - dispatch(getMappings({ index: selectedIndex, dataSourceId })) - .unwrap() - .then((resp: IndexMappings) => { - const { processorId, inputMapEntry } = getProcessorInfo( - props.uiConfig, - values - ); - if (processorId !== undefined && inputMapEntry !== undefined) { - // set/overwrite default text field for the input map. may be empty. - if (inputMapEntry !== undefined) { - const textFieldFormPath = `ingest.enrich.${processorId}.input_map.0.0.value`; - const curTextField = getIn(values, textFieldFormPath) as string; - if (!Object.keys(resp.properties).includes(curTextField)) { - const defaultTextField = - Object.keys(resp.properties).find((fieldName) => { - return resp.properties[fieldName]?.type === 'text'; - }) || ''; - setFieldValue(textFieldFormPath, defaultTextField); - } - } - } - }); - } - } - }, [selectedIndex]); - - // hook to clear out the selected index when switching options - useEffect(() => { - if (selectedOption !== SOURCE_OPTIONS.EXISTING_INDEX) { - setSelectedIndex(undefined); - } - }, [selectedOption]); - // hook to listen when the docs form value changes. useEffect(() => { if (values?.ingest?.docs) { @@ -159,10 +91,10 @@ export function SourceData(props: SourceDataProps) { <> {isEditModalOpen && ( )} @@ -234,7 +166,7 @@ export function SourceData(props: SourceDataProps) { // helper fn to parse out some useful info from the ML ingest processor config, if applicable // takes on the assumption the first processor is an ML inference processor, and should // only be executed for workflows coming from preset vector search use cases. -function getProcessorInfo( +export function getProcessorInfo( uiConfig: WorkflowConfig, values: WorkflowFormValues ): { 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 32287bed..29c9dd5f 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 @@ -24,19 +24,36 @@ import { } from '@elastic/eui'; import { JsonField } from '../input_fields'; import { + customStringify, + FETCH_ALL_QUERY, IConfigField, + IndexMappings, IngestDocsFormValues, + isVectorSearchUseCase, + SearchHit, SOURCE_OPTIONS, + Workflow, + WorkflowConfig, WorkflowFormValues, } from '../../../../../common'; -import { AppState } from '../../../../store'; -import { getFieldSchema, getInitialValue } from '../../../../utils'; +import { + AppState, + getMappings, + searchIndex, + useAppDispatch, +} from '../../../../store'; +import { + getDataSourceId, + getFieldSchema, + getInitialValue, +} from '../../../../utils'; +import { getProcessorInfo } from './source_data'; interface SourceDataProps { + workflow: Workflow | undefined; + uiConfig: WorkflowConfig; selectedOption: SOURCE_OPTIONS; setSelectedOption: (option: SOURCE_OPTIONS) => void; - selectedIndex: string | undefined; - setSelectedIndex: (index: string) => void; setIsModalOpen: (isOpen: boolean) => void; } @@ -44,6 +61,8 @@ interface SourceDataProps { * Modal for configuring the source data for ingest. */ export function SourceDataModal(props: SourceDataProps) { + const dispatch = useAppDispatch(); + const dataSourceId = getDataSourceId(); const { values, setFieldValue } = useFormikContext(); const indices = useSelector((state: AppState) => state.opensearch.indices); @@ -57,11 +76,23 @@ export function SourceDataModal(props: SourceDataProps) { } as IConfigField), }); - // persist standalone values. update when there is changes detected to the parent form + // persist standalone values. update / initialize when it is first opened const [tempDocs, setTempDocs] = useState('[]'); + // useEffect(() => { + // setTempDocs(getIn(values, 'ingest.docs')); + // }, [getIn(values, 'ingest.docs')]); + + // selected index state + const [selectedIndex, setSelectedIndex] = useState( + undefined + ); + + // hook to clear out the selected index when switching options useEffect(() => { - setTempDocs(getIn(values, 'ingest.docs')); - }, [getIn(values, 'ingest.docs')]); + if (props.selectedOption !== SOURCE_OPTIONS.EXISTING_INDEX) { + setSelectedIndex(undefined); + } + }, [props.selectedOption]); function onClose() { props.setIsModalOpen(false); @@ -71,6 +102,34 @@ export function SourceDataModal(props: SourceDataProps) { // 1. Update the form with the temp docs setFieldValue('ingest.docs', tempDocs); + // 2. Update several form values if an index is selected (and if vector search) + if (selectedIndex !== undefined) { + if (isVectorSearchUseCase(props.workflow)) { + dispatch(getMappings({ index: selectedIndex, dataSourceId })) + .unwrap() + .then((resp: IndexMappings) => { + const { processorId, inputMapEntry } = getProcessorInfo( + props.uiConfig, + values + ); + if (processorId !== undefined && inputMapEntry !== undefined) { + // set/overwrite default text field for the input map. may be empty. + if (inputMapEntry !== undefined) { + const textFieldFormPath = `ingest.enrich.${processorId}.input_map.0.0.value`; + const curTextField = getIn(values, textFieldFormPath) as string; + if (!Object.keys(resp.properties).includes(curTextField)) { + const defaultTextField = + Object.keys(resp.properties).find((fieldName) => { + return resp.properties[fieldName]?.type === 'text'; + }) || ''; + setFieldValue(textFieldFormPath, defaultTextField); + } + } + } + }); + } + } + props.setIsModalOpen(false); } @@ -83,11 +142,40 @@ export function SourceDataModal(props: SourceDataProps) { validate={(values) => {}} > {(formikProps) => { - // internal hook to loop back and update tempDocs when form changes are detected + // override to parent form value when changes detected + useEffect(() => { + formikProps.setFieldValue('docs', getIn(values, 'ingest.docs')); + }, [getIn(values, 'ingest.docs')]); + + // update tempDocs when form changes are detected useEffect(() => { + console.log('setting temp docs from internal form...'); setTempDocs(getIn(formikProps.values, 'docs')); }, [getIn(formikProps.values, 'docs')]); + // fetch sample documents if an existing index is chosen + useEffect(() => { + if (selectedIndex !== undefined) { + dispatch( + searchIndex({ + apiBody: { + index: selectedIndex, + body: FETCH_ALL_QUERY, + searchPipeline: '_none', + }, + dataSourceId, + }) + ) + .unwrap() + .then((resp) => { + const docObjs = resp.hits?.hits + ?.slice(0, 5) + ?.map((hit: SearchHit) => hit?._source); + formikProps.setFieldValue('docs', customStringify(docObjs)); + }); + } + }, [selectedIndex]); + return ( onClose()} style={{ width: '70vw' }}> @@ -179,9 +267,9 @@ export function SourceDataModal(props: SourceDataProps) { disabled: false, } as EuiSuperSelectOption) )} - valueOfSelected={props.selectedIndex} + valueOfSelected={selectedIndex} onChange={(option) => { - props.setSelectedIndex(option); + setSelectedIndex(option); }} isInvalid={false} /> From dd51259737a52f7373c0e839f801a5eaa3442b52 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 29 Oct 2024 15:57:08 -0700 Subject: [PATCH 4/5] add loading/error state Signed-off-by: Tyler Ohlsen --- .../ingest_inputs/source_data_modal.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) 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 29c9dd5f..f22466aa 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 @@ -6,6 +6,7 @@ import React, { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { Formik, getIn, useFormikContext } from 'formik'; +import { isEmpty } from 'lodash'; import * as yup from 'yup'; import { EuiSmallButton, @@ -58,7 +59,8 @@ interface SourceDataProps { } /** - * Modal for configuring the source data for ingest. + * Modal for configuring the source data for ingest. Maintains standalone form state, and only updates + * parent form if the user explicitly clicks "Update", and there are no validation errors. */ export function SourceDataModal(props: SourceDataProps) { const dispatch = useAppDispatch(); @@ -78,9 +80,10 @@ export function SourceDataModal(props: SourceDataProps) { // persist standalone values. update / initialize when it is first opened const [tempDocs, setTempDocs] = useState('[]'); - // useEffect(() => { - // setTempDocs(getIn(values, 'ingest.docs')); - // }, [getIn(values, 'ingest.docs')]); + const [tempErrors, setTempErrors] = useState(false); + + // button updating state + const [isUpdating, setIsUpdating] = useState(false); // selected index state const [selectedIndex, setSelectedIndex] = useState( @@ -99,6 +102,7 @@ export function SourceDataModal(props: SourceDataProps) { } function onUpdate() { + setIsUpdating(true); // 1. Update the form with the temp docs setFieldValue('ingest.docs', tempDocs); @@ -123,13 +127,15 @@ export function SourceDataModal(props: SourceDataProps) { return resp.properties[fieldName]?.type === 'text'; }) || ''; setFieldValue(textFieldFormPath, defaultTextField); + setIsUpdating(false); } } } }); } + } else { + setIsUpdating(false); } - props.setIsModalOpen(false); } @@ -149,11 +155,10 @@ export function SourceDataModal(props: SourceDataProps) { // update tempDocs when form changes are detected useEffect(() => { - console.log('setting temp docs from internal form...'); setTempDocs(getIn(formikProps.values, 'docs')); }, [getIn(formikProps.values, 'docs')]); - // fetch sample documents if an existing index is chosen + // fetch & populate sample documents if an existing index is chosen useEffect(() => { if (selectedIndex !== undefined) { dispatch( @@ -176,6 +181,11 @@ export function SourceDataModal(props: SourceDataProps) { } }, [selectedIndex]); + // update tempErrors if errors detected + useEffect(() => { + setTempErrors(!isEmpty(formikProps.errors)); + }, [formikProps.errors]); + return ( onClose()} style={{ width: '70vw' }}> @@ -296,6 +306,8 @@ export function SourceDataModal(props: SourceDataProps) { onUpdate()} + isLoading={isUpdating} + isDisabled={tempErrors} // blocking update until valid input is given fill={true} color="primary" data-testid="updateSourceDataButton" From a8daf08efa1eca8030c86b7bcc16fa2234c6ac1f Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 29 Oct 2024 16:32:43 -0700 Subject: [PATCH 5/5] revert testid change Signed-off-by: Tyler Ohlsen --- .../workflow_inputs/ingest_inputs/source_data_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f22466aa..f871ce1b 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 @@ -300,7 +300,7 @@ export function SourceDataModal(props: SourceDataProps) { onClick={() => onClose()} fill={false} color="primary" - data-testid="cancelSourceDataButton" + data-testid="closeSourceDataButton" > Cancel