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,