diff --git a/common/interfaces.ts b/common/interfaces.ts index 7b19704b..8c642bb6 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -47,10 +47,13 @@ export type ProcessorsConfig = { export type IndexConfig = { name: IConfigField; + mappings: IConfigField; + settings: IConfigField; }; export type IngestConfig = { - source: IConfig; + enabled: boolean; + source: {}; enrich: ProcessorsConfig; index: IndexConfig; }; diff --git a/public/pages/workflow_detail/resizable_workspace.tsx b/public/pages/workflow_detail/resizable_workspace.tsx index e28bcc13..08a2eedb 100644 --- a/public/pages/workflow_detail/resizable_workspace.tsx +++ b/public/pages/workflow_detail/resizable_workspace.tsx @@ -21,11 +21,11 @@ import { APP_PATH, uiConfigToFormik, uiConfigToSchema } from '../../utils'; import { AppState, setDirty, useAppDispatch } from '../../store'; import { WorkflowInputs } from './workflow_inputs'; import { Workspace } from './workspace'; +import { Tools } from './tools'; // styling import './workspace/workspace-styles.scss'; import '../../global-styles.scss'; -import { Tools } from './tools'; interface ResizableWorkspaceProps { workflow?: Workflow; @@ -55,6 +55,9 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { const [formValues, setFormValues] = useState({}); const [formSchema, setFormSchema] = useState(yup.object({})); + // ingest state + const [ingestDocs, setIngestDocs] = useState(''); + // Temp UI config state. For persisting changes to the UI config that may // not be saved in the backend (e.g., adding / removing an ingest processor) const [uiConfig, setUiConfig] = useState( @@ -78,6 +81,9 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { // ingest state const [ingestResponse, setIngestResponse] = useState(''); + // query state + const [queryResponse, setQueryResponse] = useState(''); + // Tools side panel state const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(true); const collapseFnVertical = useRef( @@ -117,7 +123,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { // Initialize the form state based on the current UI config useEffect(() => { if (uiConfig) { - const initFormValues = uiConfigToFormik(uiConfig); + const initFormValues = uiConfigToFormik(uiConfig, ingestDocs); const initFormSchema = uiConfigToSchema(uiConfig); setFormValues(initFormValues); setFormSchema(initFormSchema); @@ -178,6 +184,9 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { uiConfig={uiConfig} setUiConfig={setUiConfig} setIngestResponse={setIngestResponse} + setQueryResponse={setQueryResponse} + ingestDocs={ingestDocs} + setIngestDocs={setIngestDocs} /> @@ -250,6 +259,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { diff --git a/public/pages/workflow_detail/tools/tools.tsx b/public/pages/workflow_detail/tools/tools.tsx index e363d238..9465a37b 100644 --- a/public/pages/workflow_detail/tools/tools.tsx +++ b/public/pages/workflow_detail/tools/tools.tsx @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { isEmpty } from 'lodash'; import { EuiCodeEditor, EuiFlexGroup, @@ -20,6 +21,7 @@ import { Resources } from './resources'; interface ToolsProps { workflow?: Workflow; ingestResponse: string; + queryResponse: string; } enum TAB_ID { @@ -58,6 +60,20 @@ const inputTabs = [ export function Tools(props: ToolsProps) { const [selectedTabId, setSelectedTabId] = useState(TAB_ID.INGEST); + // auto-navigate to ingest response if a populated value has been set, indicating ingest has been ran + useEffect(() => { + if (!isEmpty(props.ingestResponse)) { + setSelectedTabId(TAB_ID.INGEST); + } + }, [props.ingestResponse]); + + // auto-navigate to query response if a populated value has been set, indicating search has been ran + useEffect(() => { + if (!isEmpty(props.queryResponse)) { + setSelectedTabId(TAB_ID.QUERY); + } + }, [props.queryResponse]); + return ( {}} readOnly={true} setOptions={{ fontSize: '12px', @@ -111,7 +127,19 @@ export function Tools(props: ToolsProps) { /> )} {selectedTabId === TAB_ID.QUERY && ( - TODO: Run queries placeholder + )} {selectedTabId === TAB_ID.ERRORS && ( TODO: View errors placeholder diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/advanced_settings.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/advanced_settings.tsx new file mode 100644 index 00000000..50335b25 --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/advanced_settings.tsx @@ -0,0 +1,50 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiAccordion, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { IConfigField, WorkflowConfig } from '../../../../../common'; +import { JsonField } from '../input_fields'; + +interface AdvancedSettingsProps { + uiConfig: WorkflowConfig; + onFormChange: () => void; +} + +/** + * Input component for configuring ingest-side advanced settings + */ +export function AdvancedSettings(props: AdvancedSettingsProps) { + return ( + + + + + + + + + + + + + + + + ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx index 86fecf9c..3e975771 100644 --- a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_data.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { IConfigField, WorkflowConfig } from '../../../../../common'; import { TextField } from '../input_fields'; +import { AdvancedSettings } from './advanced_settings'; interface IngestDataProps { uiConfig: WorkflowConfig; @@ -31,6 +32,12 @@ export function IngestData(props: IngestDataProps) { onFormChange={props.onFormChange} /> + + + ); } diff --git a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx index 017e6ca6..6a341cc1 100644 --- a/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/ingest_inputs/ingest_inputs.tsx @@ -12,8 +12,7 @@ import { WorkflowConfig } from '../../../../../common'; interface IngestInputsProps { onFormChange: () => void; - ingestDocs: {}[]; - setIngestDocs: (docs: {}[]) => void; + setIngestDocs: (docs: string) => void; uiConfig: WorkflowConfig; setUiConfig: (uiConfig: WorkflowConfig) => void; } @@ -22,12 +21,14 @@ interface IngestInputsProps { * The base component containing all of the ingest-related inputs */ export function IngestInputs(props: IngestInputsProps) { + // TODO: add some toggle to enable/disable ingest altogether. + // UX not finalized on where that will live currently return ( 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 2d73be21..b1fa01e6 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 @@ -3,33 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; -import { - EuiCodeEditor, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, -} from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { useFormikContext } from 'formik'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { JsonField } from '../input_fields'; +import { IConfigField, WorkspaceFormValues } from '../../../../../common'; interface SourceDataProps { - ingestDocs: {}[]; - setIngestDocs: (docs: {}[]) => void; + setIngestDocs: (docs: string) => void; + onFormChange: () => void; } /** * Input component for configuring the source data for ingest. */ export function SourceData(props: SourceDataProps) { - const [jsonStr, setJsonStr] = useState('{}'); + const { values } = useFormikContext(); + // Hook to listen when the docs form value changes. + // Try to set the ingestDocs if possible useEffect(() => { - try { - const json = JSON.parse(jsonStr); - props.setIngestDocs([json]); - } catch (e) { - props.setIngestDocs([]); + if (values?.ingest?.docs) { + props.setIngestDocs(values.ingest.docs); } - }, [jsonStr]); + }, [values?.ingest?.docs]); return ( @@ -39,21 +36,18 @@ export function SourceData(props: SourceDataProps) { - { - setJsonStr(input); - }} - readOnly={false} - setOptions={{ - fontSize: '14px', - }} - aria-label="Code Editor" - tabSize={2} + 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 73177bc0..08fc2d2e 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 @@ -3,26 +3,90 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { EuiText, EuiTextArea } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { Field, FieldProps, getIn, useFormikContext } from 'formik'; +import { EuiCodeEditor, EuiFormRow, EuiLink, EuiText } from '@elastic/eui'; +import { IConfigField, WorkspaceFormValues } from '../../../../../common'; interface JsonFieldProps { - label: string; - placeholder: string; + field: IConfigField; + fieldPath: string; // the full path in string-form to the field (e.g., 'ingest.enrich.processors.text_embedding_processor.inputField') + onFormChange: () => void; + editorHeight?: string; } /** * An input field for a component where users manually enter * in some custom JSON */ -// TODO: integrate with formik export function JsonField(props: JsonFieldProps) { + const { errors, touched, values } = useFormikContext(); + + // temp input state. only format when users click out of the code editor + const [jsonStr, setJsonStr] = useState('{}'); + + // initializing the text to be the stringified form value + useEffect(() => { + if (props.fieldPath && values) { + const formValue = getIn(values, props.fieldPath) as string; + if (formValue) { + setJsonStr(formValue); + } + } + }, [props.fieldPath, values]); + return ( - <> - - {props.label} - - - + + {({ field, form }: FieldProps) => { + return ( + + + Learn more + + + ) : undefined + } + helpText={props.field.helpText || undefined} + error={getIn(errors, field.name)} + isInvalid={getIn(errors, field.name) && getIn(touched, field.name)} + > + { + setJsonStr(input); + }} + onBlur={() => { + try { + form.setFieldValue( + field.name, + JSON.stringify(JSON.parse(jsonStr), undefined, 2) + ); + } catch (error) { + form.setFieldValue(field.name, jsonStr); + } finally { + form.setFieldTouched(field.name); + props.onFormChange(); + } + }} + readOnly={false} + setOptions={{ + fontSize: '14px', + }} + aria-label="Code Editor" + tabSize={2} + /> + + ); + }} + ); } diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx index ea4aa2a6..4fbecbad 100644 --- a/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx +++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/configure_search_request.tsx @@ -3,15 +3,39 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { + EuiCodeEditor, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiTitle, +} from '@elastic/eui'; +import { Field, FieldProps } from 'formik'; -interface ConfigureSearchRequestProps {} +interface ConfigureSearchRequestProps { + setQuery: (query: {}) => void; +} /** * Input component for configuring a search request */ export function ConfigureSearchRequest(props: ConfigureSearchRequestProps) { + const indexFieldPath = 'ingest.index.name'; + + // query state + const [queryStr, setQueryStr] = useState('{}'); + + useEffect(() => { + try { + const query = JSON.parse(queryStr); + props.setQuery(query); + } catch (e) { + props.setQuery({}); + } + }, [queryStr]); + return ( @@ -20,7 +44,45 @@ export function ConfigureSearchRequest(props: ConfigureSearchRequestProps) { - TODO + + + + {({ field, form }: FieldProps) => { + return ( + // TODO: make this dynamic depending on if ingest is defined or not. + // 1/ (incomplete) if no ingest, make this a dropdown to select existing indices + // 2/ (complete) if ingest, show the defined index from ingest config, make it readonly + + + + ); + }} + + + + { + setQueryStr(input); + }} + readOnly={false} + setOptions={{ + fontSize: '14px', + }} + aria-label="Code Editor" + tabSize={2} + /> + + ); diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/search_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/search_inputs.tsx index 2249c3e2..314dd0c5 100644 --- a/public/pages/workflow_detail/workflow_inputs/search_inputs/search_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/search_inputs.tsx @@ -13,6 +13,7 @@ import { WorkflowConfig } from '../../../../../common'; interface SearchInputsProps { uiConfig: WorkflowConfig; setUiConfig: (uiConfig: WorkflowConfig) => void; + setQuery: (query: {}) => void; onFormChange: () => void; } @@ -23,7 +24,7 @@ export function SearchInputs(props: SearchInputsProps) { return ( - + diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 73fddad3..e619c5be 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useFormikContext } from 'formik'; import { isEmpty } from 'lodash'; import { @@ -29,6 +29,7 @@ import { getWorkflow, ingest, provisionWorkflow, + searchIndex, updateWorkflow, useAppDispatch, } from '../../../store'; @@ -37,6 +38,7 @@ import { formikToUiConfig, reduceToTemplate, configToTemplateFlows, + hasProvisionedIngestResources, } from '../../../utils'; // styling @@ -48,6 +50,9 @@ interface WorkflowInputsProps { uiConfig: WorkflowConfig | undefined; setUiConfig: (uiConfig: WorkflowConfig) => void; setIngestResponse: (ingestResponse: string) => void; + setQueryResponse: (queryResponse: string) => void; + ingestDocs: string; + setIngestDocs: (docs: string) => void; } export enum STEP { @@ -70,7 +75,19 @@ export function WorkflowInputs(props: WorkflowInputsProps) { const [selectedStep, setSelectedStep] = useState(STEP.INGEST); // ingest state - const [ingestDocs, setIngestDocs] = useState<{}[]>([]); + const [ingestProvisioned, setIngestProvisioned] = useState(false); + + // query state + const [query, setQuery] = useState<{}>({}); + + // maintain global states + const onIngest = selectedStep === STEP.INGEST; + const onIngestAndProvisioned = onIngest && ingestProvisioned; + const onIngestAndUnprovisioned = onIngest && !ingestProvisioned; + + useEffect(() => { + setIngestProvisioned(hasProvisionedIngestResources(props.workflow)); + }, [props.workflow]); // Utility fn to update the workflow, including any updated/new resources // Eventually, should be able to use fine-grained provisioning to do a single API call @@ -152,18 +169,20 @@ export function WorkflowInputs(props: WorkflowInputsProps) { return success; } - // TODO: running props.validateAndSubmit() will need to be ran before every ingest and - // search, if the form is dirty / values have changed. This will update the workflow if needed. - // Note that the temporary data (the ingest docs and the search query) will not need to be persisted - // in the form (need to confirm if query-side / using search template, will need to persist something) async function validateAndRunIngestion(): Promise { let success = false; try { - if (ingestDocs.length > 0 && !isEmpty(ingestDocs[0])) { + let ingestDocsObjs = [] as {}[]; + try { + // TODO: test with multiple objs, make sure parsing logic works + const ingestDocObj = JSON.parse(props.ingestDocs); + ingestDocsObjs = [ingestDocObj]; + } catch (e) {} + if (ingestDocsObjs.length > 0 && !isEmpty(ingestDocsObjs[0])) { success = await validateAndUpdateWorkflow(); if (success) { const indexName = values.ingest.index.name; - const doc = ingestDocs[0]; + const doc = ingestDocsObjs[0]; dispatch(ingest({ index: indexName, doc })) .unwrap() .then(async (resp) => { @@ -181,13 +200,35 @@ export function WorkflowInputs(props: WorkflowInputsProps) { } catch (error) { console.error('Error ingesting documents: ', error); } - return success; } - function validateAndRunQuery(): void { - console.log('running query...'); - validateAndUpdateWorkflow(); + async function validateAndRunQuery(): Promise { + let success = false; + try { + if (!isEmpty(query)) { + success = await validateAndUpdateWorkflow(); + if (success) { + const indexName = values.ingest.index.name; + dispatch(searchIndex({ index: indexName, body: query })) + .unwrap() + .then(async (resp) => { + const hits = resp.hits.hits; + props.setQueryResponse(JSON.stringify(hits, undefined, 2)); + }) + .catch((error: any) => { + getCore().notifications.toasts.addDanger(error); + props.setQueryResponse(''); + throw error; + }); + } + } else { + getCore().notifications.toasts.addDanger('No valid query provided'); + } + } catch (error) { + console.error('Error running query: ', error); + } + return success; } return ( @@ -223,8 +264,10 @@ export function WorkflowInputs(props: WorkflowInputsProps) {

- {selectedStep === STEP.INGEST + {onIngestAndUnprovisioned ? 'Define ingest pipeline' + : onIngestAndProvisioned + ? 'Edit ingest pipeline' : 'Define search pipeline'}

@@ -236,11 +279,10 @@ export function WorkflowInputs(props: WorkflowInputsProps) { overflowX: 'hidden', }} > - {selectedStep === STEP.INGEST ? ( + {onIngest ? ( @@ -248,6 +290,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { )} @@ -259,7 +302,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
- {selectedStep === STEP.INGEST ? ( + {onIngestAndUnprovisioned ? ( <> + ) : onIngestAndProvisioned ? ( + <> + + { + validateAndRunIngestion(); + }} + > + Run ingestion + + + + setSelectedStep(STEP.SEARCH)} + > + {`Next >`} + + + ) : ( <> diff --git a/public/pages/workflows/new_workflow/utils.ts b/public/pages/workflows/new_workflow/utils.ts index 6f1749ae..4fb41fe4 100644 --- a/public/pages/workflows/new_workflow/utils.ts +++ b/public/pages/workflows/new_workflow/utils.ts @@ -67,11 +67,8 @@ function fetchEmptyMetadata(): UIState { return { config: { ingest: { - source: { - id: 'source', - name: 'Source', - fields: [], - }, + enabled: true, + source: {}, enrich: { processors: [], }, @@ -81,6 +78,16 @@ function fetchEmptyMetadata(): UIState { type: 'string', label: 'Index name', }, + mappings: { + id: 'indexMappings', + type: 'json', + label: 'Index mappings', + }, + settings: { + id: 'indexSettings', + type: 'json', + label: 'Index settings', + }, }, }, search: { diff --git a/public/utils/config_to_form_utils.ts b/public/utils/config_to_form_utils.ts index 84966c06..9a3679fb 100644 --- a/public/utils/config_to_form_utils.ts +++ b/public/utils/config_to_form_utils.ts @@ -21,18 +21,26 @@ import { **************** Config -> formik utils ********************** */ -export function uiConfigToFormik(config: WorkflowConfig): WorkflowFormValues { +// if the user has input any ingest docs, persist them in the form. +// we don't persist in the config as it logically does not belong there, +// and can be extremely large. so we pass that as a standalone field +export function uiConfigToFormik( + config: WorkflowConfig, + ingestDocs: string +): WorkflowFormValues { const formikValues = {} as WorkflowFormValues; - formikValues['ingest'] = ingestConfigToFormik(config.ingest); + formikValues['ingest'] = ingestConfigToFormik(config.ingest, ingestDocs); formikValues['search'] = searchConfigToFormik(config.search); return formikValues; } function ingestConfigToFormik( - ingestConfig: IngestConfig | undefined + ingestConfig: IngestConfig | undefined, + ingestDocs: string ): FormikValues { let ingestFormikValues = {} as FormikValues; if (ingestConfig) { + ingestFormikValues['docs'] = ingestDocs || getInitialValue('json'); ingestFormikValues['enrich'] = processorsConfigToFormik( ingestConfig.enrich ); @@ -65,6 +73,10 @@ function indexConfigToFormik(indexConfig: IndexConfig): FormikValues { let formValues = {} as FormikValues; formValues['name'] = indexConfig.name.value || getInitialValue(indexConfig.name.type); + formValues['mappings'] = + indexConfig.mappings.value || getInitialValue(indexConfig.mappings.type); + formValues['settings'] = + indexConfig.settings.value || getInitialValue(indexConfig.settings.type); return formValues; } @@ -105,7 +117,7 @@ export function getInitialValue(fieldType: ConfigFieldType): ConfigFieldValue { return []; } case 'json': { - return {}; + return '{}'; } } } diff --git a/public/utils/config_to_schema_utils.ts b/public/utils/config_to_schema_utils.ts index f1469cd0..f8a89622 100644 --- a/public/utils/config_to_schema_utils.ts +++ b/public/utils/config_to_schema_utils.ts @@ -12,8 +12,8 @@ import { SearchConfig, ProcessorsConfig, WorkflowSchemaObj, - IConfigField, IndexConfig, + ConfigFieldType, } from '../../common'; /* @@ -32,7 +32,7 @@ function ingestConfigToSchema( ): ObjectSchema { const ingestSchemaObj = {} as { [key: string]: Schema }; if (ingestConfig) { - // TODO: implement for the other sub-categories + ingestSchemaObj['docs'] = getFieldSchema('json'); ingestSchemaObj['enrich'] = processorsConfigToSchema(ingestConfig.enrich); ingestSchemaObj['index'] = indexConfigToSchema(ingestConfig.index); } @@ -44,7 +44,7 @@ function processorsConfigToSchema(processorsConfig: ProcessorsConfig): Schema { processorsConfig.processors.forEach((processorConfig) => { const processorSchemaObj = {} as { [key: string]: Schema }; processorConfig.fields.forEach((field) => { - processorSchemaObj[field.id] = getFieldSchema(field); + processorSchemaObj[field.id] = getFieldSchema(field.type); }); processorsSchemaObj[processorConfig.id] = yup.object(processorSchemaObj); }); @@ -54,7 +54,9 @@ function processorsConfigToSchema(processorsConfig: ProcessorsConfig): Schema { function indexConfigToSchema(indexConfig: IndexConfig): Schema { const indexSchemaObj = {} as { [key: string]: Schema }; - indexSchemaObj['name'] = getFieldSchema(indexConfig.name); + indexSchemaObj['name'] = getFieldSchema(indexConfig.name.type); + indexSchemaObj['mappings'] = getFieldSchema(indexConfig.mappings.type); + indexSchemaObj['settings'] = getFieldSchema(indexConfig.settings.type); return yup.object(indexSchemaObj); } @@ -71,9 +73,9 @@ function searchConfigToSchema( **************** Yup (validation) utils ********************** */ -function getFieldSchema(field: IConfigField): Schema { +function getFieldSchema(fieldType: ConfigFieldType): Schema { let baseSchema: Schema; - switch (field.type) { + switch (fieldType) { case 'string': case 'select': { baseSchema = yup.string().min(1, 'Too short').max(70, 'Too long'); @@ -100,7 +102,16 @@ function getFieldSchema(field: IConfigField): Schema { break; } case 'json': { - baseSchema = yup.object().json(); + baseSchema = yup.string().test('json', 'Invalid JSON', (value) => { + try { + // @ts-ignore + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }); + break; } } diff --git a/public/utils/config_to_template_utils.ts b/public/utils/config_to_template_utils.ts index 590eb073..1cf4d80a 100644 --- a/public/utils/config_to_template_utils.ts +++ b/public/utils/config_to_template_utils.ts @@ -179,15 +179,8 @@ function indexConfigToTemplateNode( ingestPipelineNode?: CreateIngestPipelineNode, searchPipelineNode?: CreateSearchPipelineNode ): CreateIndexNode { - const indexName = indexConfig.name.value as string; - - // TODO: extract model details to determine the mappings - const finalIndexMappings = { - properties: {}, - } as IndexMappings; - + let finalSettings = indexConfig.settings.value as {}; let finalPreviousNodeInputs = {}; - let finalSettings = {}; function updateFinalInputsAndSettings( createPipelineNode: @@ -222,10 +215,10 @@ function indexConfigToTemplateNode( type: WORKFLOW_STEP_TYPE.CREATE_INDEX_STEP_TYPE, previous_node_inputs: finalPreviousNodeInputs, user_inputs: { - index_name: indexName, + index_name: indexConfig.name.value as string, configurations: { settings: finalSettings, - mappings: finalIndexMappings, + mappings: indexConfig.mappings.value as IndexMappings, }, }, }; diff --git a/public/utils/form_to_config_utils.ts b/public/utils/form_to_config_utils.ts index e45f9ab5..d124c122 100644 --- a/public/utils/form_to_config_utils.ts +++ b/public/utils/form_to_config_utils.ts @@ -60,6 +60,8 @@ function formikToIndexUiConfig( existingConfig: IndexConfig ): IndexConfig { existingConfig['name'].value = indexFormValues['name']; + existingConfig['mappings'].value = indexFormValues['mappings']; + existingConfig['settings'].value = indexFormValues['settings']; return existingConfig; } diff --git a/public/utils/utils.ts b/public/utils/utils.ts index be44339e..35aabf6c 100644 --- a/public/utils/utils.ts +++ b/public/utils/utils.ts @@ -4,7 +4,7 @@ */ import { EuiFilterSelectItem } from '@elastic/eui'; -import { WORKFLOW_STATE } from '../../common'; +import { WORKFLOW_STATE, WORKFLOW_STEP_TYPE, Workflow } from '../../common'; // Append 16 random characters export function generateId(prefix: string): string { @@ -39,3 +39,19 @@ export function getStateOptions(): EuiFilterSelectItem[] { } as EuiFilterSelectItem, ]; } + +export function hasProvisionedIngestResources( + workflow: Workflow | undefined +): boolean { + let result = false; + workflow?.resourcesCreated?.some((resource) => { + if ( + resource.stepType === + WORKFLOW_STEP_TYPE.CREATE_INGEST_PIPELINE_STEP_TYPE || + resource.stepType === WORKFLOW_STEP_TYPE.CREATE_INDEX_STEP_TYPE + ) { + result = true; + } + }); + return result; +}