diff --git a/common/interfaces.ts b/common/interfaces.ts
index 357373ca..aa31c7d2 100644
--- a/common/interfaces.ts
+++ b/common/interfaces.ts
@@ -27,6 +27,7 @@ export type MDSQueryParams = {
export type ConfigFieldType =
| 'string'
+ | 'textArea'
| 'json'
| 'jsonArray'
| 'jsonString'
diff --git a/public/app.tsx b/public/app.tsx
index 132bcc07..0bedd168 100644
--- a/public/app.tsx
+++ b/public/app.tsx
@@ -29,7 +29,6 @@ interface Props extends RouteComponentProps {
export const FlowFrameworkDashboardsApp = (props: Props) => {
const { setHeaderActionMenu } = props;
-
// Render the application DOM.
return (
void;
setActionMenu: (menuMount?: MountPoint) => void;
+ setBlockNavigation: (blockNavigation: boolean) => void;
}
export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
const dispatch = useAppDispatch();
- const history = useHistory();
const { resetForm, setTouched, values, touched, dirty } = useFormikContext<
WorkflowFormValues
>();
@@ -116,12 +115,6 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
};
}, [setHeaderVariant, USE_NEW_HOME_PAGE]);
- const onExitButtonClick = () => {
- history.replace(
- constructUrlWithParams(APP_PATH.WORKFLOWS, undefined, dataSourceId)
- );
- };
-
// get & render the data source component, if applicable
let DataSourceComponent: ReactElement | null = null;
if (dataSourceEnabled && getDataSourceManagementPlugin() && dataSourceId) {
@@ -200,21 +193,8 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
? ingestSaveButtonDisabled
: searchSaveButtonDisabled;
- // Add warning when exiting with unsaved changes
- function alertFn(e: BeforeUnloadEvent) {
- e.preventDefault();
- }
-
- // Hook for warning of unsaved changes if a browser refresh is detected
useEffect(() => {
- if (!saveDisabled) {
- window.addEventListener('beforeunload', alertFn);
- } else {
- window.removeEventListener('beforeunload', alertFn);
- }
- return () => {
- window.removeEventListener('beforeunload', alertFn);
- };
+ props.setBlockNavigation(!saveDisabled);
}, [saveDisabled]);
// Utility fn to update the workflow UI config only, based on the current form values.
@@ -319,7 +299,10 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
iconType: 'exit',
tooltip: 'Return to projects',
ariaLabel: 'Exit',
- run: onExitButtonClick,
+ href: constructHrefWithDataSourceId(
+ APP_PATH.WORKFLOWS,
+ dataSourceId
+ ),
controlType: 'icon',
} as TopNavMenuIconData,
{
@@ -407,15 +390,10 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
Export
,
{
- history.replace(
- constructUrlWithParams(
- APP_PATH.WORKFLOWS,
- undefined,
- dataSourceId
- )
- );
- }}
+ href={constructHrefWithDataSourceId(
+ APP_PATH.WORKFLOWS,
+ dataSourceId
+ )}
data-testid="closeButton"
>
Close
@@ -430,6 +408,7 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
{`Save`}
,
{
);
});
});
-
- test('tests navigation to workflows list on Close button click', async () => {
- const { getByTestId, history } = renderWithRouter(
- workflowId,
- workflowName,
- WORKFLOW_TYPE.CUSTOM
- );
- // The WorkflowDetail Page Close button should navigate back to the workflows list
- userEvent.click(getByTestId('closeButton'));
- await waitFor(() => {
- expect(history.location.pathname).toBe('/workflows');
- });
- });
});
describe('WorkflowDetail Page with skip ingestion option (Hybrid Search Workflow)', () => {
diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx
index 7fe3d449..33d3b16b 100644
--- a/public/pages/workflow_detail/workflow_detail.tsx
+++ b/public/pages/workflow_detail/workflow_detail.tsx
@@ -4,7 +4,7 @@
*/
import React, { useEffect, useState } from 'react';
-import { RouteComponentProps } from 'react-router-dom';
+import { Prompt, RouteComponentProps, useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { ReactFlowProvider } from 'reactflow';
import { escape } from 'lodash';
@@ -79,6 +79,7 @@ interface WorkflowDetailProps
export function WorkflowDetail(props: WorkflowDetailProps) {
const dispatch = useAppDispatch();
+ const history = useHistory();
// On initial load:
// - fetch workflow
@@ -96,6 +97,55 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
dispatch(setSearchPipelineErrors({ errors: {} }));
}, []);
+ const [blockNavigation, setBlockNavigation] = useState(false);
+
+ // 1. Block page refreshes if unsaved changes.
+ // Remove listeners on component unload.
+ function preventPageRefresh(e: BeforeUnloadEvent) {
+ e.preventDefault();
+ }
+ useEffect(() => {
+ if (blockNavigation) {
+ window.addEventListener('beforeunload', preventPageRefresh);
+ } else {
+ window.removeEventListener('beforeunload', preventPageRefresh);
+ }
+ return () => {
+ window.removeEventListener('beforeunload', preventPageRefresh);
+ };
+ }, [blockNavigation]);
+
+ // 2. Block navigation (externally-controlled buttons/links)
+ // Remove listeners on component unload.
+ const handleLinkClick = (e: Event) => {
+ const confirmation = window.confirm(
+ 'You have unsaved changes. Are you sure you want to leave?'
+ );
+ if (!confirmation) {
+ e.preventDefault();
+ }
+ };
+ useEffect(() => {
+ // try to catch as many external links as possible, particularly
+ // ones that will go to the home page, or different plugins within
+ // the side navigation.
+ const links = document.querySelectorAll(`a[href*="app/"]`);
+ if (blockNavigation) {
+ links.forEach((link) => {
+ link.addEventListener('click', handleLinkClick);
+ });
+ } else {
+ links.forEach((link) => {
+ link.removeEventListener('click', handleLinkClick);
+ });
+ }
+ return () => {
+ links.forEach((link) => {
+ link.removeEventListener('click', handleLinkClick);
+ });
+ };
+ }, [blockNavigation]);
+
// data-source-related states
const dataSourceEnabled = getDataSourceEnabled().enabled;
const dataSourceId = getDataSourceId();
@@ -188,71 +238,100 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
}
}, [uiConfig]);
- return errorMessage?.includes(ERROR_GETTING_WORKFLOW_MSG) ||
- errorMessage?.includes(NO_TEMPLATES_FOUND_MSG) ? (
-
-
- Oops! We couldn't find that workflow}
- titleSize="s"
- />
-
-
-
- Return to home
-
-
-
- ) : (
- {}}
- validate={(values) => {}}
- >
-
-
-
-
-
+ {/**
+ * 3. Block navigation (internally-controlled buttons/links). context is confined to navigation checks
+ * within the plugin-defined router.
+ */}
+ {
+ const confirmation = window.confirm(
+ 'You have unsaved changes. Are you sure you want to leave?'
+ );
+ if (!confirmation) {
+ setBlockNavigation(true);
+ history.goBack();
+ return false;
+ } else {
+ setBlockNavigation(false);
+ return true;
+ }
+ }}
+ />
+
+ {errorMessage?.includes(ERROR_GETTING_WORKFLOW_MSG) ||
+ errorMessage?.includes(NO_TEMPLATES_FOUND_MSG) ? (
+
+
+ Oops! We couldn't find that workflow}
+ titleSize="s"
/>
-
-
-
-
+
+
+
+ Return to home
+
+
+
+ ) : (
+ {}}
+ validate={(values) => {}}
+ >
+
+
+
+
+
+
+
+
+
+ )}
+ >
);
}
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 4704c5ac..c4680dc9 100644
--- a/public/pages/workflow_detail/workflow_inputs/config_field_list.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/config_field_list.tsx
@@ -50,6 +50,20 @@ export function ConfigFieldList(props: ConfigFieldListProps) {
);
break;
}
+ case 'textArea': {
+ el = (
+
+
+
+
+ );
+ break;
+ }
case 'select': {
el = (
diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/normalization_processor_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/normalization_processor_inputs.tsx
index a0637b9e..396ff750 100644
--- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/normalization_processor_inputs.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/normalization_processor_inputs.tsx
@@ -22,7 +22,8 @@ interface NormalizationProcessorInputsProps {
}
/**
- * Specialized component to render the normalization processor. Adds some helper text around weights field.
+ * Specialized component to render the normalization processor. Adds some helper text around weights field,
+ * and bubble it up as a primary field to populate (even though it is technically optional).
* In the future, may have a more customizable / guided way for specifying the array of weights.
* For example, could have some visual way of linking it to the underlying sub-queries in the query field,
* enforce its length = the number of queries, etc.
@@ -38,30 +39,30 @@ export function NormalizationProcessorInputs(
);
return (
- // We only have optional fields for this processor, so everything is nested under the accordion
-
-
+ <>
+
+
+
-
-
+
-
-
-
-
+
+ >
);
}
diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx
index 9e63accd..7a60e3fd 100644
--- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx
@@ -804,6 +804,8 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
disabled={
!ingestEnabled
? false
+ : onIngestAndUnprovisioned
+ ? true
: ingestTemplatesDifferent
}
iconSide="right"
diff --git a/public/pages/workflows/new_workflow/utils.ts b/public/pages/workflows/new_workflow/utils.ts
index 57d983a7..067170d6 100644
--- a/public/pages/workflows/new_workflow/utils.ts
+++ b/public/pages/workflows/new_workflow/utils.ts
@@ -228,9 +228,7 @@ export function fetchHybridSearchMetadata(version: string): UIState {
);
baseState.config.search.enrichResponse.processors = [
- injectDefaultWeightsInNormalizationProcessor(
- new NormalizationProcessor().toObj()
- ),
+ new NormalizationProcessor().toObj(),
];
baseState.config.search.enrichRequest.processors = isPreV219
@@ -304,25 +302,3 @@ function injectQueryTemplateInProcessor(
);
return processorConfig;
}
-
-// set default weights for a normalization processor. assumes there is 2 queries, and equally
-// balances the weight. We don't hardcode in the configuration, since we don't want to set
-// invalid defaults for arbitrary use cases (e.g., more than 2 queries). In this case, we
-// are already setting 2 queries by default, so we can make this assumption.
-function injectDefaultWeightsInNormalizationProcessor(
- processorConfig: IProcessorConfig
-): IProcessorConfig {
- processorConfig.optionalFields = processorConfig.optionalFields?.map(
- (optionalField) => {
- let updatedField = optionalField;
- if (optionalField.id === 'weights') {
- updatedField = {
- ...updatedField,
- value: '0.5, 0.5',
- };
- }
- return updatedField;
- }
- );
- return processorConfig;
-}
diff --git a/public/utils/config_to_form_utils.ts b/public/utils/config_to_form_utils.ts
index b1ed4d45..ffeef207 100644
--- a/public/utils/config_to_form_utils.ts
+++ b/public/utils/config_to_form_utils.ts
@@ -125,6 +125,7 @@ function searchIndexConfigToFormik(
export function getInitialValue(fieldType: ConfigFieldType): ConfigFieldValue {
switch (fieldType) {
case 'string':
+ case 'textArea':
case 'select':
case 'jsonLines': {
return '';
diff --git a/public/utils/config_to_schema_utils.ts b/public/utils/config_to_schema_utils.ts
index 0044fd19..64e52c6f 100644
--- a/public/utils/config_to_schema_utils.ts
+++ b/public/utils/config_to_schema_utils.ts
@@ -152,6 +152,7 @@ export function getFieldSchema(
switch (field.type) {
case 'string':
+ case 'textArea':
case 'select': {
baseSchema = defaultStringSchema;
break;