From 611d0a315f6587d19fb160eff80f5631f350007a Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 10 Sep 2024 10:53:00 -0700 Subject: [PATCH 1/5] Remove autosave; add save/revert buttons (ingest) Signed-off-by: Tyler Ohlsen --- .../workflow_inputs/workflow_inputs.tsx | 188 +++++++++++------- 1 file changed, 115 insertions(+), 73 deletions(-) diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 952ab5a7..559c415e 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { getIn, useFormikContext } from 'formik'; -import { debounce, isEmpty, isEqual } from 'lodash'; +import { isEmpty, isEqual } from 'lodash'; import { EuiSmallButton, EuiSmallButtonEmpty, @@ -24,6 +24,7 @@ import { EuiStepsHorizontal, EuiText, EuiTitle, + EuiSmallButtonIcon, } from '@elastic/eui'; import { MAX_WORKFLOW_NAME_TO_DISPLAY, @@ -96,14 +97,17 @@ export function WorkflowInputs(props: WorkflowInputsProps) { const { submitForm, validateForm, + resetForm, setFieldValue, + setTouched, values, touched, } = useFormikContext(); const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); - // running ingest/search state + // transient running states + const [isRunningSave, setIsRunningSave] = useState(false); const [isRunningIngest, setIsRunningIngest] = useState(false); const [isRunningSearch, setIsRunningSearch] = useState(false); const [isRunningDelete, setIsRunningDelete] = useState(false); @@ -129,7 +133,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { isEmpty(getIn(values, 'search.enrichResponse')); // maintaining any fine-grained differences between the generated templates produced by the form, - // and the one persisted in the workflow itself. We enable/disable buttons + // produced by the current UI config, and the one persisted in the workflow itself. We enable/disable buttons // based on any discrepancies found. const [persistedTemplateNodes, setPersistedTemplateNodes] = useState< TemplateNode[] @@ -159,6 +163,20 @@ export function WorkflowInputs(props: WorkflowInputsProps) { const [searchTemplatesDifferent, setSearchTemplatesDifferent] = useState< boolean >(false); + const [unsavedIngestProcessors, setUnsavedIngestProcessors] = useState< + boolean + >(false); + + // listener when ingest processors have been added/deleted. + // compare to the indexed/persisted workflow config + useEffect(() => { + setUnsavedIngestProcessors( + !isEqual( + props.uiConfig?.ingest?.enrich?.processors, + props.workflow?.ui_metadata?.config?.ingest?.enrich?.processors + ) + ); + }, [props.uiConfig?.ingest?.enrich?.processors?.length]); // fetch the total template nodes useEffect(() => { @@ -234,75 +252,63 @@ export function WorkflowInputs(props: WorkflowInputsProps) { formGeneratedSearchTemplateNodes, ]); - // Auto-save the UI metadata when users update form values. - // Only update the underlying workflow template (deprovision/provision) when - // users explicitly run ingest/search and need to have updated resources - // to test against. - // We use useCallback() with an autosave flag that is only set within the fn itself. - // This is so we can fetch the latest values (uiConfig, formik values) inside a memoized fn, - // but only when we need to. - const [autosave, setAutosave] = useState(false); - function triggerAutosave(): void { - setAutosave(!autosave); - } - const debounceAutosave = useCallback( - debounce(async () => { - triggerAutosave(); - }, 1000), - [autosave] - ); - - // Hook to execute autosave when triggered. Runs the update API with update_fields set to true, - // to update the ui_metadata without updating the underlying template for a provisioned workflow. useEffect(() => { - (async () => { - if (!isEmpty(touched)) { - const updatedTemplate = { - name: props.workflow?.name, - ui_metadata: { - ...props.workflow?.ui_metadata, - config: formikToUiConfig(values, props.uiConfig as WorkflowConfig), - }, - } as WorkflowTemplate; - await dispatch( - updateWorkflow({ - apiBody: { + setIngestProvisioned(hasProvisionedIngestResources(props.workflow)); + }, [props.workflow]); + + // Utility fn to update the workflow template only. A get workflow API call is subsequently run + // to fetch the updated state. + async function updateWorkflowTemplate() { + setIsRunningSave(true); + const updatedTemplate = { + name: props.workflow?.name, + ui_metadata: { + ...props.workflow?.ui_metadata, + config: formikToUiConfig(values, props.uiConfig as WorkflowConfig), + }, + } as WorkflowTemplate; + await dispatch( + updateWorkflow({ + apiBody: { + workflowId: props.workflow?.id as string, + workflowTemplate: updatedTemplate, + updateFields: true, + reprovision: false, + }, + dataSourceId, + }) + ) + .unwrap() + .then(async (result) => { + setUnsavedIngestProcessors(false); + setTouched({}); + new Promise((f) => setTimeout(f, 1000)).then(async () => { + dispatch( + getWorkflow({ workflowId: props.workflow?.id as string, - workflowTemplate: updatedTemplate, - updateFields: true, - reprovision: false, - }, - dataSourceId, - }) - ) - .unwrap() - .then(async (result) => { - // TODO: figure out clean way to update the "last updated" - // section. The problem with re-fetching this every time, is it - // triggers lots of component rebuilds due to the base workflow prop - // changing. - // get any updates after autosave - // new Promise((f) => setTimeout(f, 1000)).then(async () => { - // dispatch(getWorkflow(props.workflow?.id as string)); - // }); - }) - .catch((error: any) => { - console.error('Error autosaving workflow: ', error); - }); - } - })(); - }, [autosave]); + dataSourceId, + }) + ); + }); + }) + .catch((error: any) => { + console.error('Error autosaving workflow: ', error); + }) + .finally(() => { + setIsRunningSave(false); + }); + } - // Hook to listen for changes to form values and trigger autosave - useEffect(() => { - if (!isEmpty(values)) { - debounceAutosave(); + // Utility fn to revert any unsaved changes, reset the form + function revertUnsavedChanges(): void { + resetForm(); + if ( + unsavedIngestProcessors && + props.workflow?.ui_metadata?.config !== undefined + ) { + props.setUiConfig(props.workflow?.ui_metadata?.config); } - }, [values]); - - useEffect(() => { - setIngestProvisioned(hasProvisionedIngestResources(props.workflow)); - }, [props.workflow]); + } // Utility fn to update the workflow, including any updated/new resources. // The reprovision param is used to determine whether we are doing full @@ -327,6 +333,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { .unwrap() .then(async (result) => { await sleep(1000); + setUnsavedIngestProcessors(false); success = true; // Kicking off an async task to re-fetch the workflow details // after some amount of time. Provisioning will finish in an indeterminate @@ -370,6 +377,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { .unwrap() .then(async (result) => { await sleep(1000); + setUnsavedIngestProcessors(false); await dispatch( provisionWorkflow({ workflowId: updatedWorkflow.id as string, @@ -431,11 +439,10 @@ export function WorkflowInputs(props: WorkflowInputsProps) { ...(includeSearch && search !== undefined ? { search } : {}), }; if (Object.keys(relevantValidationResults).length > 0) { - // TODO: may want to persist more fine-grained form validation (ingest vs. search) - // For example, running an ingest should be possible, even with some - // invalid query or search processor config. And vice versa. + getCore().notifications.toasts.addDanger('Missing or invalid fields'); console.error('Form invalid'); } else { + setTouched({}); const updatedConfig = formikToUiConfig( values, props.uiConfig as WorkflowConfig @@ -759,6 +766,41 @@ export function WorkflowInputs(props: WorkflowInputsProps) { ) : onIngest ? ( <> + + { + revertUnsavedChanges(); + }} + /> + + + { + updateWorkflowTemplate(); + }} + > + {`Save`} + + - Run ingestion + Build and run ingestion From cb9ef9ac06bf44c86c785081e95c9eb36b360da6 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 10 Sep 2024 11:15:30 -0700 Subject: [PATCH 2/5] Add save/revert buttons (search) Signed-off-by: Tyler Ohlsen --- .../workflow_inputs/workflow_inputs.tsx | 72 +++++++++++++++++-- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 559c415e..56689cc2 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -166,6 +166,9 @@ export function WorkflowInputs(props: WorkflowInputsProps) { const [unsavedIngestProcessors, setUnsavedIngestProcessors] = useState< boolean >(false); + const [unsavedSearchProcessors, setUnsavedSearchProcessors] = useState< + boolean + >(false); // listener when ingest processors have been added/deleted. // compare to the indexed/persisted workflow config @@ -178,6 +181,25 @@ export function WorkflowInputs(props: WorkflowInputsProps) { ); }, [props.uiConfig?.ingest?.enrich?.processors?.length]); + // listener when search processors have been added/deleted. + // compare to the indexed/persisted workflow config + useEffect(() => { + setUnsavedSearchProcessors( + !isEqual( + props.uiConfig?.search?.enrichRequest?.processors, + props.workflow?.ui_metadata?.config?.search?.enrichRequest?.processors + ) || + !isEqual( + props.uiConfig?.search?.enrichResponse?.processors, + props.workflow?.ui_metadata?.config?.search?.enrichResponse + ?.processors + ) + ); + }, [ + props.uiConfig?.search?.enrichRequest?.processors?.length, + props.uiConfig?.search?.enrichResponse?.processors?.length, + ]); + // fetch the total template nodes useEffect(() => { setPersistedTemplateNodes( @@ -281,6 +303,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { .unwrap() .then(async (result) => { setUnsavedIngestProcessors(false); + setUnsavedSearchProcessors(false); setTouched({}); new Promise((f) => setTimeout(f, 1000)).then(async () => { dispatch( @@ -303,7 +326,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { function revertUnsavedChanges(): void { resetForm(); if ( - unsavedIngestProcessors && + (unsavedIngestProcessors || unsavedSearchProcessors) && props.workflow?.ui_metadata?.config !== undefined ) { props.setUiConfig(props.workflow?.ui_metadata?.config); @@ -334,6 +357,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { .then(async (result) => { await sleep(1000); setUnsavedIngestProcessors(false); + setUnsavedSearchProcessors(false); success = true; // Kicking off an async task to re-fetch the workflow details // after some amount of time. Provisioning will finish in an indeterminate @@ -378,6 +402,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { .then(async (result) => { await sleep(1000); setUnsavedIngestProcessors(false); + setUnsavedSearchProcessors(false); await dispatch( provisionWorkflow({ workflowId: updatedWorkflow.id as string, @@ -771,10 +796,10 @@ export function WorkflowInputs(props: WorkflowInputsProps) { iconType="editorUndo" aria-label="undo changes" isDisabled={ - unsavedIngestProcessors - ? false - : isRunningSave || isRunningIngest + isRunningSave || isRunningIngest ? true + : unsavedIngestProcessors + ? false : isEmpty(touched?.ingest?.enrich) && isEmpty(touched?.ingest?.index) } @@ -786,10 +811,10 @@ export function WorkflowInputs(props: WorkflowInputsProps) { + + { + revertUnsavedChanges(); + }} + /> + + + { + updateWorkflowTemplate(); + }} + > + {`Save`} + + Date: Tue, 10 Sep 2024 11:43:04 -0700 Subject: [PATCH 3/5] cleanup Signed-off-by: Tyler Ohlsen --- .../workflow_inputs/workflow_inputs.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 56689cc2..23296c56 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -278,9 +278,9 @@ export function WorkflowInputs(props: WorkflowInputsProps) { setIngestProvisioned(hasProvisionedIngestResources(props.workflow)); }, [props.workflow]); - // Utility fn to update the workflow template only. A get workflow API call is subsequently run + // Utility fn to update the workflow UI config only. A get workflow API call is subsequently run // to fetch the updated state. - async function updateWorkflowTemplate() { + async function updateWorkflowUiConfig() { setIsRunningSave(true); const updatedTemplate = { name: props.workflow?.name, @@ -820,7 +820,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { } isLoading={isRunningSave} onClick={() => { - updateWorkflowTemplate(); + updateWorkflowUiConfig(); }} > {`Save`} @@ -892,7 +892,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { } isLoading={isRunningSave} onClick={() => { - updateWorkflowTemplate(); + updateWorkflowUiConfig(); }} > {`Save`} @@ -911,7 +911,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { validateAndRunQuery(); }} > - Run query + Build and run query From 6297bd9e9d187de27678cb55cf4719606197aa1a Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 10 Sep 2024 11:45:45 -0700 Subject: [PATCH 4/5] cleanup Signed-off-by: Tyler Ohlsen --- .../pages/workflow_detail/workflow_inputs/workflow_inputs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 23296c56..5b64defc 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -315,7 +315,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) { }); }) .catch((error: any) => { - console.error('Error autosaving workflow: ', error); + console.error('Error saving workflow: ', error); }) .finally(() => { setIsRunningSave(false); From 1a7aa2302f42cb9fd56d21e4e77cebfbafacd021 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 10 Sep 2024 11:55:31 -0700 Subject: [PATCH 5/5] remove unnecessary update Signed-off-by: Tyler Ohlsen --- public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 5b64defc..926a7795 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -655,7 +655,6 @@ export function WorkflowInputs(props: WorkflowInputsProps) { .unwrap() .then(async (result) => { setFieldValue('ingest.enabled', false); - await validateAndUpdateWorkflow(false); // @ts-ignore await dispatch( getWorkflow({