From 138fe605c947acd0d86ebac6f314956e8405cf0a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 7 Dec 2024 02:15:37 +0000 Subject: [PATCH] Simplify form navigation (#515) Signed-off-by: Tyler Ohlsen (cherry picked from commit d1025193b08186294d5467d8aee2ac0bd08890ee) Signed-off-by: github-actions[bot] --- .../components/export_modal.tsx | 19 +- .../workflow_detail/components/header.tsx | 2 + .../workflow_detail/resizable_workspace.tsx | 1 - .../workflow_detail/tools/query/query.tsx | 18 +- .../workflow_detail/workflow_detail.test.tsx | 13 +- .../workflow_inputs/config_field_list.tsx | 1 + .../input_fields/boolean_field.tsx | 33 +- .../configure_search_request.tsx | 13 +- .../search_inputs/search_inputs.tsx | 7 +- .../workflow_inputs/workflow_inputs.tsx | 518 ++++++++---------- .../workflow_list/delete_workflow_modal.tsx | 4 +- .../workflows/workflow_list/workflow_list.tsx | 4 +- public/utils/config_to_template_utils.ts | 30 +- 13 files changed, 333 insertions(+), 330 deletions(-) diff --git a/public/pages/workflow_detail/components/export_modal.tsx b/public/pages/workflow_detail/components/export_modal.tsx index 9c3aaffb..300a72a3 100644 --- a/public/pages/workflow_detail/components/export_modal.tsx +++ b/public/pages/workflow_detail/components/export_modal.tsx @@ -18,6 +18,8 @@ import { EuiModalBody, EuiModalFooter, EuiSmallButtonEmpty, + EuiCallOut, + EuiSpacer, } from '@elastic/eui'; import { CREATE_WORKFLOW_LINK, @@ -29,6 +31,8 @@ import { reduceToTemplate } from '../../../utils'; interface ExportModalProps { workflow?: Workflow; + unsavedIngestProcessors: boolean; + unsavedSearchProcessors: boolean; setIsExportModalOpen(isOpen: boolean): void; } @@ -78,14 +82,25 @@ export function ExportModal(props: ExportModalProps) { > -

{`Export ${getCharacterLimitedString( +

{`Export '${getCharacterLimitedString( props.workflow?.name || '', 25 - )}`}

+ )}'`}

+ {(props.unsavedIngestProcessors || props.unsavedSearchProcessors) && ( + <> + + + + )} {`To build out identical resources in other environments, create and provision a workflow using the below template.`}{' '} diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx index 32955f2c..e319a458 100644 --- a/public/pages/workflow_detail/components/header.tsx +++ b/public/pages/workflow_detail/components/header.tsx @@ -276,6 +276,8 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) { {isExportModalOpen && ( )} diff --git a/public/pages/workflow_detail/resizable_workspace.tsx b/public/pages/workflow_detail/resizable_workspace.tsx index d681b720..b7976c6d 100644 --- a/public/pages/workflow_detail/resizable_workspace.tsx +++ b/public/pages/workflow_detail/resizable_workspace.tsx @@ -126,7 +126,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { uiConfig={props.uiConfig} setUiConfig={props.setUiConfig} setIngestResponse={setIngestResponse} - setQueryResponse={setQueryResponse} ingestDocs={props.ingestDocs} setIngestDocs={props.setIngestDocs} isRunningIngest={props.isRunningIngest} diff --git a/public/pages/workflow_detail/tools/query/query.tsx b/public/pages/workflow_detail/tools/query/query.tsx index 8f1b542e..ba617465 100644 --- a/public/pages/workflow_detail/tools/query/query.tsx +++ b/public/pages/workflow_detail/tools/query/query.tsx @@ -67,7 +67,8 @@ export function Query(props: QueryProps) { // Standalone / sandboxed search request state. Users can test things out // without updating the base form / persisted value. We default to different values - // based on the context (ingest or search). + // based on the context (ingest or search), and update based on changes to the context + // (ingest v. search), or if the parent form values are changed. const [tempRequest, setTempRequest] = useState(''); useEffect(() => { setTempRequest( @@ -76,6 +77,14 @@ export function Query(props: QueryProps) { : values?.search?.request || '{}' ); }, [props.selectedStep]); + useEffect(() => { + if ( + !isEmpty(values?.search?.request) && + props.selectedStep === CONFIG_STEP.SEARCH + ) { + setTempRequest(values?.search?.request); + } + }, [values?.search?.request]); // state for if to execute search w/ or w/o any configured search pipeline. // default based on if there is an available search pipeline or not. @@ -87,13 +96,6 @@ export function Query(props: QueryProps) { // query params state const [queryParams, setQueryParams] = useState([]); - // listen for changes to the upstream / form query, and reset the default - useEffect(() => { - if (!isEmpty(values?.search?.request)) { - setTempRequest(values?.search?.request); - } - }, [values?.search?.request]); - // Do a few things when the request is changed: // 1. Check if there is a new set of query parameters, and if so, // reset the form. diff --git a/public/pages/workflow_detail/workflow_detail.test.tsx b/public/pages/workflow_detail/workflow_detail.test.tsx index 24359bc4..34fc1c81 100644 --- a/public/pages/workflow_detail/workflow_detail.test.tsx +++ b/public/pages/workflow_detail/workflow_detail.test.tsx @@ -83,7 +83,6 @@ describe('WorkflowDetail Page with create ingestion option', () => { expect( getAllByText((content) => content.startsWith('Last updated:')).length ).toBeGreaterThan(0); - expect(getAllByText('Search pipeline').length).toBeGreaterThan(0); expect(getByText('Close')).toBeInTheDocument(); expect(getByText('Export')).toBeInTheDocument(); expect(getByText('Visual')).toBeInTheDocument(); @@ -93,10 +92,6 @@ describe('WorkflowDetail Page with create ingestion option', () => { expect(getByRole('tab', { name: 'Errors' })).toBeInTheDocument(); expect(getByRole('tab', { name: 'Resources' })).toBeInTheDocument(); - // "Run ingestion" button exists - const runIngestionButton = getByTestId('runIngestionButton'); - expect(runIngestionButton).toBeInTheDocument(); - // "Search pipeline" button should be disabled by default const searchPipelineButton = getByTestId('searchPipelineButton'); expect(searchPipelineButton).toBeInTheDocument(); @@ -119,7 +114,7 @@ describe('WorkflowDetail Page Functionality (Custom Workflow)', () => { // Export button opens the export component userEvent.click(getByTestId('exportButton')); await waitFor(() => { - expect(getByText(`Export ${workflowName}`)).toBeInTheDocument(); + expect(getByText(`Export '${workflowName}'`)).toBeInTheDocument(); }); // Close the export component @@ -179,8 +174,7 @@ describe('WorkflowDetail Page with skip ingestion option (Hybrid Search Workflow ); // Defining a new ingest pipeline & index is enabled by default - const enabledCheckbox = getByTestId('checkbox-ingest.enabled'); - expect(enabledCheckbox).toBeChecked(); + const enabledCheckbox = getByTestId('switch-ingest.enabled'); // Skipping ingest pipeline and navigating to search userEvent.click(enabledCheckbox); @@ -190,7 +184,7 @@ describe('WorkflowDetail Page with skip ingestion option (Hybrid Search Workflow // Search pipeline await waitFor(() => { - expect(getAllByText('Define search pipeline').length).toBeGreaterThan(0); + expect(getAllByText('Define search flow').length).toBeGreaterThan(0); }); expect(getAllByText('Configure query').length).toBeGreaterThan(0); @@ -224,7 +218,6 @@ describe('WorkflowDetail Page with skip ingestion option (Hybrid Search Workflow }); // Build and Run query, Back buttons are present - expect(getByTestId('runQueryButton')).toBeInTheDocument(); const searchPipelineBackButton = getByTestId('searchPipelineBackButton'); userEvent.click(searchPipelineBackButton); diff --git a/public/pages/workflow_detail/workflow_inputs/config_field_list.tsx b/public/pages/workflow_detail/workflow_inputs/config_field_list.tsx index 7065621b..8515eb5c 100644 --- a/public/pages/workflow_detail/workflow_inputs/config_field_list.tsx +++ b/public/pages/workflow_detail/workflow_inputs/config_field_list.tsx @@ -63,6 +63,7 @@ export function ConfigFieldList(props: ConfigFieldListProps) { diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/boolean_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/boolean_field.tsx index 63521d28..5487f85e 100644 --- a/public/pages/workflow_detail/workflow_inputs/input_fields/boolean_field.tsx +++ b/public/pages/workflow_detail/workflow_inputs/input_fields/boolean_field.tsx @@ -10,15 +10,19 @@ import { EuiFlexGroup, EuiFlexItem, EuiIconTip, + EuiSwitch, EuiText, } from '@elastic/eui'; interface BooleanFieldProps { fieldPath: string; // the full path in string-form to the field (e.g., 'ingest.enrich.processors.text_embedding_processor.inputField') label: string; + type: ComponentType; helpText?: string; } +type ComponentType = 'Checkbox' | 'Switch'; + /** * An input field for a boolean value. Implemented as an EuiCompressedRadioGroup with 2 mutually exclusive options. */ @@ -26,7 +30,7 @@ export function BooleanField(props: BooleanFieldProps) { return ( {({ field, form }: FieldProps) => { - return ( + return props.type === 'Checkbox' ? ( + ) : ( + + + + {props.label} + + {props.helpText && ( + + + + )} + + + } + checked={field.value === undefined || field.value === true} + onChange={() => { + form.setFieldValue(field.name, !field.value); + form.setFieldTouched(field.name, true); + }} + /> ); }} 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 fea99ff3..422b55dc 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 @@ -21,10 +21,7 @@ import { WorkflowFormValues } from '../../../../../common'; import { AppState } from '../../../../store'; import { EditQueryModal } from './edit_query_modal'; -interface ConfigureSearchRequestProps { - setQuery: (query: string) => void; - setQueryResponse: (queryResponse: string) => void; -} +interface ConfigureSearchRequestProps {} /** * Input component for configuring a search request @@ -55,14 +52,6 @@ export function ConfigureSearchRequest(props: ConfigureSearchRequestProps) { // Edit modal state const [isEditModalOpen, setIsEditModalOpen] = useState(false); - // Hook to listen when the query form value changes. - // Try to set the query request if possible - useEffect(() => { - if (values?.search?.request) { - props.setQuery(values.search.request); - } - }, [values?.search?.request]); - return ( <> {isEditModalOpen && ( 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 9ac13b15..c922a6ac 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 @@ -18,8 +18,6 @@ import { getDataSourceId } from '../../../../utils'; interface SearchInputsProps { uiConfig: WorkflowConfig; setUiConfig: (uiConfig: WorkflowConfig) => void; - setQuery: (query: string) => void; - setQueryResponse: (queryResponse: string) => void; } /** @@ -38,10 +36,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 ec993635..9794e624 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -13,19 +13,15 @@ import { EuiFlexItem, EuiHorizontalRule, EuiLoadingSpinner, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, EuiPanel, - EuiStepsHorizontal, EuiText, + EuiHealth, + EuiBottomBar, + EuiIconTip, + EuiSmallButtonIcon, } from '@elastic/eui'; import { CONFIG_STEP, - MAX_WORKFLOW_NAME_TO_DISPLAY, - SearchHit, TemplateNode, WORKFLOW_STEP_TYPE, Workflow, @@ -33,7 +29,6 @@ import { WorkflowFormValues, WorkflowTemplate, customStringify, - getCharacterLimitedString, } from '../../../../common'; import { IngestInputs } from './ingest_inputs'; import { SearchInputs } from './search_inputs'; @@ -42,7 +37,6 @@ import { deprovisionWorkflow, getWorkflow, provisionWorkflow, - searchIndex, updateWorkflow, useAppDispatch, } from '../../../store'; @@ -68,7 +62,6 @@ interface WorkflowInputsProps { uiConfig: WorkflowConfig | undefined; setUiConfig: (uiConfig: WorkflowConfig) => void; setIngestResponse: (ingestResponse: string) => void; - setQueryResponse: (queryResponse: string) => void; ingestDocs: string; setIngestDocs: (docs: string) => void; isRunningIngest: boolean; @@ -91,26 +84,23 @@ export function WorkflowInputs(props: WorkflowInputsProps) { const { submitForm, validateForm, - setFieldValue, + resetForm, setTouched, values, + dirty, } = useFormikContext(); const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); - // query state - const [query, setQuery] = useState(''); - // transient running states - const [isRunningDelete, setIsRunningDelete] = useState(false); + const [isUpdatingSearchPipeline, setIsUpdatingSearchPipeline] = useState< + boolean + >(false); // provisioned resources states const [ingestProvisioned, setIngestProvisioned] = useState(false); const [searchProvisioned, setSearchProvisioned] = useState(false); - // confirm modal state - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - // last ingested state const [lastIngested, setLastIngested] = useState( undefined @@ -120,13 +110,20 @@ export function WorkflowInputs(props: WorkflowInputsProps) { const onIngest = props.selectedStep === CONFIG_STEP.INGEST; const onSearch = props.selectedStep === CONFIG_STEP.SEARCH; const ingestEnabled = values?.ingest?.enabled || false; - const onIngestAndProvisioned = onIngest && ingestProvisioned; - const onSearchAndProvisioned = onSearch && searchProvisioned; const onIngestAndUnprovisioned = onIngest && !ingestProvisioned; const onIngestAndDisabled = onIngest && !ingestEnabled; const isProposingNoSearchResources = isEmpty(getIn(values, 'search.enrichRequest')) && isEmpty(getIn(values, 'search.enrichResponse')); + // fine-grained deprovisioning is not supported, hence once a search pipeline is created, it cannot + // be deleted without re-creating all of the resources, including ingest resources. + const searchUpdateDisabled = + searchProvisioned && isProposingNoSearchResources; + // there is an edge case where search resources based on the form should be ignored: + // that is, when users first create a workflow and are setting up ingest for the first time, + // where there may be preset form values for search, but should be ignored during the initial ingestion provisioning steps. + const includeSearchDuringProvision = + onSearch || (onIngest && searchProvisioned); // maintaining any fine-grained differences between the generated templates produced by the form, // produced by the current UI config, and the one persisted in the workflow itself. We enable/disable buttons @@ -171,7 +168,9 @@ export function WorkflowInputs(props: WorkflowInputsProps) { props.uiConfig && props.workflow && configToTemplateFlows( - formikToUiConfig(values, props.uiConfig as WorkflowConfig) + formikToUiConfig(values, props.uiConfig as WorkflowConfig), + true, + includeSearchDuringProvision ).provision.nodes) || [] ); @@ -251,19 +250,21 @@ export function WorkflowInputs(props: WorkflowInputsProps) { setDocsPopulated(parsedDocsObjs.length > 0 && !isEmpty(parsedDocsObjs[0])); }, [props.ingestDocs]); - // maintain global states (button eligibility) - const ingestRunButtonDisabled = !ingestTemplatesDifferent || !docsPopulated; - const ingestToSearchButtonDisabled = - ingestTemplatesDifferent || props.isRunningIngest; - const searchBackButtonDisabled = - props.isRunningSearch || - (isProposingNoSearchResources || !ingestProvisioned - ? false - : searchTemplatesDifferent); - const searchRunButtonDisabled = - props.isRunningSearch || - (isProposingNoSearchResources && - hasProvisionedSearchResources(props.workflow)); + // bottom bar eligibility + const showIngestBottomBar = + onIngest && + docsPopulated && + (ingestTemplatesDifferent || props.isRunningIngest); + const showSearchBottomBar = + onSearch && (searchTemplatesDifferent || isUpdatingSearchPipeline); + + // Utility fn to revert any unsaved changes, reset the form + function revertUnsavedChanges(): void { + resetForm(); + if (props.workflow?.ui_metadata?.config !== undefined) { + props.setUiConfig(props.workflow?.ui_metadata?.config); + } + } // Utility fn to update the workflow UI config only, based on the current form values. // A get workflow API call is subsequently run to fetch the updated state. @@ -455,7 +456,16 @@ export function WorkflowInputs(props: WorkflowInputsProps) { ...props.workflow?.ui_metadata, config: updatedConfig, }, - workflows: configToTemplateFlows(updatedConfig), + // for updating the actual template to be provisioned, we need to handle few scenarios: + // for ingest, always set to "true", as we will always take into account any ingest config + // into the provisioning steps + // for search, we generally want to include, except for the edge case of initial workflow creation. + // see description where "includeSearchDuringProvision" is defined. + workflows: configToTemplateFlows( + updatedConfig, + true, + includeSearchDuringProvision + ), } as Workflow; success = await updateWorkflowAndResources( updatedWorkflow, @@ -511,56 +521,26 @@ export function WorkflowInputs(props: WorkflowInputsProps) { return success; } - // Updating search. If existing ingest resources, run fine-grained provisioning to persist that + // Updating search-related resources. If existing ingest resources, run fine-grained provisioning to persist that // created index and any indexed data, and only update/re-create the search // pipeline, as well as adding that pipeline as the default pipeline for the existing index. // If no ingest resources (using user's existing index), run full // deprovision/update/provision, since we're just re-creating the search pipeline. // This logic is propagated by passing `reprovision=true/false` in the // validateAndUpdateWorkflow() fn calls below. - async function validateAndRunQuery(): Promise { - props.setIsRunningSearch(true); + async function validateAndUpdateSearchResources(): Promise { + setIsUpdatingSearchPipeline(true); let success = false; try { - let queryObj = {}; - try { - queryObj = JSON.parse(query); - } catch (e) {} - if (!isEmpty(queryObj)) { - if (hasProvisionedIngestResources(props.workflow)) { - success = await validateAndUpdateWorkflow(true, false, true); - } else { - success = await validateAndUpdateWorkflow(false, false, true); - } - - if (success) { - const indexName = values.search.index.name; - dispatch( - searchIndex({ - apiBody: { index: indexName, body: query }, - dataSourceId, - }) - ) - .unwrap() - .then(async (resp) => { - props.setQueryResponse( - customStringify( - resp?.hits?.hits?.map((hit: SearchHit) => hit._source) - ) - ); - }) - .catch((error: any) => { - props.setQueryResponse(''); - throw error; - }); - } + if (hasProvisionedIngestResources(props.workflow)) { + success = await validateAndUpdateWorkflow(true, false, true); } else { - getCore().notifications.toasts.addDanger('No valid query provided'); + success = await validateAndUpdateWorkflow(false, false, true); } } catch (error) { - console.error('Error running query: ', error); + console.error('Error updating search pipeline: ', error); } - props.setIsRunningSearch(false); + setIsUpdatingSearchPipeline(false); return success; } @@ -584,93 +564,25 @@ export function WorkflowInputs(props: WorkflowInputsProps) { }} > - {}, - }, - { - title: CONFIG_STEP.SEARCH, - isComplete: false, - isSelected: onSearch, - onClick: () => {}, - }, - ]} - /> - {isDeleteModalOpen && ( - setIsDeleteModalOpen(false)} - > - - -

{`Delete resources for workflow ${getCharacterLimitedString( - props.workflow?.name || '', - MAX_WORKFLOW_NAME_TO_DISPLAY - )}?`}

-
-
- - - The resources for this workflow will be permanently deleted. - This action cannot be undone. - - - - setIsDeleteModalOpen(false)} - > - {' '} - Cancel - - { - setIsRunningDelete(true); - await dispatch( - // If in the future we want to start with a fresh/empty state after deleting resources, - // will need to update the workflow with an empty UI config before re-fetching the workflow. - // For now we still persist the config, just clean up / deprovision the resources. - deprovisionWorkflow({ - apiBody: { - workflowId: props.workflow?.id as string, - resourceIds: getResourcesToBeForceDeleted( - props.workflow - ), - }, - dataSourceId, - }) - ) - .unwrap() - .then(async (result) => { - props.setSelectedStep(CONFIG_STEP.INGEST); - setFieldValue('ingest.enabled', false); - // @ts-ignore - await dispatch( - getWorkflow({ - workflowId: props.workflow?.id as string, - dataSourceId, - }) - ); - }) - .catch((error: any) => {}) - .finally(() => { - setIsDeleteModalOpen(false); - setIsRunningDelete(false); - }); - }} - fill={true} - color="danger" - > - Delete resources - - -
- )} + + + +

+ {onIngest ? 'Define ingestion flow' : 'Define search flow'} +

+
+
+ {onIngestAndUnprovisioned && ( + + + + )} +
- - -

- {onIngestAndUnprovisioned ? ( - 'Define ingest pipeline' - ) : onIngestAndProvisioned || onSearchAndProvisioned ? ( - - - {onIngestAndProvisioned - ? `Edit ingest pipeline` - : `Edit search pipeline`} - - - - - setIsDeleteModalOpen(true)} - iconType="trash" - iconSide="left" - > - Delete resources - - - {onSearchAndProvisioned && ( - - { - props.displaySearchPanel(); - }} - > - Test pipeline - - - )} - - - - ) : ( - 'Define search pipeline' - )} -

-
-
{onIngest ? ( <> - {onIngestAndUnprovisioned && ( - <> - - - )} {!onIngestAndDisabled && ( )}
@@ -760,79 +616,94 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
- - {onIngest && !ingestEnabled ? ( - - { - props.setSelectedStep(CONFIG_STEP.SEARCH); - }} - data-testid="searchPipelineButton" - iconSide="right" - iconType="arrowRight" - > - {`Search pipeline`} - - - ) : onIngest ? ( + + {onIngest && ( <> - { - validateAndRunIngestion(); - }} - data-testid="runIngestionButton" - disabled={ingestRunButtonDisabled} - isLoading={props.isRunningIngest} + - Build and run ingestion - + {ingestProvisioned + ? 'Active ingestion resources' + : 'No active ingestion resources'} + - { - props.setSelectedStep(CONFIG_STEP.SEARCH); - }} - data-testid="searchPipelineButton" - disabled={ingestToSearchButtonDisabled} - iconSide="right" - iconType="arrowRight" + - {`Search pipeline`} - + {ingestTemplatesDifferent && ( + + + + )} + + + { + props.setSelectedStep(CONFIG_STEP.SEARCH); + }} + data-testid="searchPipelineButton" + disabled={ingestTemplatesDifferent} + iconSide="right" + iconType="arrowRight" + > + {`Next: Search pipeline`} + + + - ) : ( + )} + {onSearch && ( <> - - props.setSelectedStep(CONFIG_STEP.INGEST) - } - data-testid="searchPipelineBackButton" - iconSide="left" - iconType="arrowLeft" - > - Ingest pipeline - + + + + props.setSelectedStep(CONFIG_STEP.INGEST) + } + /> + + + + {ingestProvisioned + ? 'Active ingestion resources' + : 'No active ingestion resources'} + + + - - { - validateAndRunQuery(); - }} - data-testid="runQueryButton" + + - Build and run query - + {searchProvisioned + ? 'Active search pipeline' + : 'No active search pipeline'} + )} @@ -842,6 +713,97 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
)} + {showIngestBottomBar && ( + + + + You have pending changes + + + + {!props.isRunningIngest && ( + + revertUnsavedChanges()} + > + Discard changes + + + )} + + validateAndRunIngestion()} + > + Update + + + + + + + )} + {showSearchBottomBar && ( + + + {searchUpdateDisabled ? ( + + + You must specify at least one processor + + + ) : ( + + You have pending changes + + )} + + + {!isUpdatingSearchPipeline && ( + + revertUnsavedChanges()} + > + Discard changes + + + )} + {!searchUpdateDisabled && ( + + { + if (await validateAndUpdateSearchResources()) { + getCore().notifications.toasts.addSuccess( + 'Search pipeline updated' + ); + props.displaySearchPanel(); + } + }} + > + Update + + + )} + + + + + )} ); } diff --git a/public/pages/workflows/workflow_list/delete_workflow_modal.tsx b/public/pages/workflows/workflow_list/delete_workflow_modal.tsx index f542ffae..4b0ab387 100644 --- a/public/pages/workflows/workflow_list/delete_workflow_modal.tsx +++ b/public/pages/workflows/workflow_list/delete_workflow_modal.tsx @@ -74,10 +74,10 @@ export function DeleteWorkflowModal(props: DeleteWorkflowModalProps) { props.clearDeleteState()}> -

{`Delete ${getCharacterLimitedString( +

{`Delete '${getCharacterLimitedString( props.workflow.name, MAX_WORKFLOW_NAME_TO_DISPLAY - )}?`}

+ )}'?`}

diff --git a/public/pages/workflows/workflow_list/workflow_list.tsx b/public/pages/workflows/workflow_list/workflow_list.tsx index e6ae4881..62bd7de3 100644 --- a/public/pages/workflows/workflow_list/workflow_list.tsx +++ b/public/pages/workflows/workflow_list/workflow_list.tsx @@ -140,10 +140,10 @@ export function WorkflowList(props: WorkflowListProps) { > -

{`Active resources with ${getCharacterLimitedString( +

{`Active resources with '${getCharacterLimitedString( selectedWorkflow.name, MAX_WORKFLOW_NAME_TO_DISPLAY - )}`}

+ )}'`}
diff --git a/public/utils/config_to_template_utils.ts b/public/utils/config_to_template_utils.ts index 7d091a3f..ea581277 100644 --- a/public/utils/config_to_template_utils.ts +++ b/public/utils/config_to_template_utils.ts @@ -45,21 +45,35 @@ import { sanitizeJSONPath } from './utils'; **************** Config -> template utils ********************** */ -export function configToTemplateFlows(config: WorkflowConfig): TemplateFlows { - const provisionFlow = configToProvisionTemplateFlow(config); +export function configToTemplateFlows( + config: WorkflowConfig, + includeIngest: boolean = true, + includeSearch: boolean = true +): TemplateFlows { + const provisionFlow = configToProvisionTemplateFlow( + config, + includeIngest, + includeSearch + ); return { provision: provisionFlow, }; } -function configToProvisionTemplateFlow(config: WorkflowConfig): TemplateFlow { +function configToProvisionTemplateFlow( + config: WorkflowConfig, + includeIngest: boolean = true, + includeSearch: boolean = true +): TemplateFlow { const nodes = [] as TemplateNode[]; const edges = [] as TemplateEdge[]; - nodes.push( - ...ingestConfigToTemplateNodes(config.ingest), - ...searchConfigToTemplateNodes(config.search) - ); + if (includeIngest) { + nodes.push(...ingestConfigToTemplateNodes(config.ingest)); + } + if (includeSearch) { + nodes.push(...searchConfigToTemplateNodes(config.search)); + } const createIngestPipelineNode = nodes.find( (node) => node.type === WORKFLOW_STEP_TYPE.CREATE_INGEST_PIPELINE_STEP_TYPE @@ -68,7 +82,7 @@ function configToProvisionTemplateFlow(config: WorkflowConfig): TemplateFlow { (node) => node.type === WORKFLOW_STEP_TYPE.CREATE_SEARCH_PIPELINE_STEP_TYPE ) as CreateSearchPipelineNode; - if (config.ingest.enabled.value) { + if (config?.ingest?.enabled?.value && includeIngest) { nodes.push( indexConfigToTemplateNode( config.ingest.index,