Skip to content

Commit eb21762

Browse files
authored
Add explicit save / revert buttons in search & ingest forms (#361)
* Remove autosave; add save/revert buttons (ingest) Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> * Add save/revert buttons (search) Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> * cleanup Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> * cleanup Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> * remove unnecessary update Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> --------- Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
1 parent 7c5ad9a commit eb21762

File tree

1 file changed

+174
-75
lines changed

1 file changed

+174
-75
lines changed

public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx

+174-75
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React, { useCallback, useEffect, useState } from 'react';
6+
import React, { useEffect, useState } from 'react';
77
import { getIn, useFormikContext } from 'formik';
8-
import { debounce, isEmpty, isEqual } from 'lodash';
8+
import { isEmpty, isEqual } from 'lodash';
99
import {
1010
EuiSmallButton,
1111
EuiSmallButtonEmpty,
@@ -24,6 +24,7 @@ import {
2424
EuiStepsHorizontal,
2525
EuiText,
2626
EuiTitle,
27+
EuiSmallButtonIcon,
2728
} from '@elastic/eui';
2829
import {
2930
MAX_WORKFLOW_NAME_TO_DISPLAY,
@@ -96,14 +97,17 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
9697
const {
9798
submitForm,
9899
validateForm,
100+
resetForm,
99101
setFieldValue,
102+
setTouched,
100103
values,
101104
touched,
102105
} = useFormikContext<WorkflowFormValues>();
103106
const dispatch = useAppDispatch();
104107
const dataSourceId = getDataSourceId();
105108

106-
// running ingest/search state
109+
// transient running states
110+
const [isRunningSave, setIsRunningSave] = useState<boolean>(false);
107111
const [isRunningIngest, setIsRunningIngest] = useState<boolean>(false);
108112
const [isRunningSearch, setIsRunningSearch] = useState<boolean>(false);
109113
const [isRunningDelete, setIsRunningDelete] = useState<boolean>(false);
@@ -129,7 +133,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
129133
isEmpty(getIn(values, 'search.enrichResponse'));
130134

131135
// maintaining any fine-grained differences between the generated templates produced by the form,
132-
// and the one persisted in the workflow itself. We enable/disable buttons
136+
// produced by the current UI config, and the one persisted in the workflow itself. We enable/disable buttons
133137
// based on any discrepancies found.
134138
const [persistedTemplateNodes, setPersistedTemplateNodes] = useState<
135139
TemplateNode[]
@@ -159,6 +163,42 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
159163
const [searchTemplatesDifferent, setSearchTemplatesDifferent] = useState<
160164
boolean
161165
>(false);
166+
const [unsavedIngestProcessors, setUnsavedIngestProcessors] = useState<
167+
boolean
168+
>(false);
169+
const [unsavedSearchProcessors, setUnsavedSearchProcessors] = useState<
170+
boolean
171+
>(false);
172+
173+
// listener when ingest processors have been added/deleted.
174+
// compare to the indexed/persisted workflow config
175+
useEffect(() => {
176+
setUnsavedIngestProcessors(
177+
!isEqual(
178+
props.uiConfig?.ingest?.enrich?.processors,
179+
props.workflow?.ui_metadata?.config?.ingest?.enrich?.processors
180+
)
181+
);
182+
}, [props.uiConfig?.ingest?.enrich?.processors?.length]);
183+
184+
// listener when search processors have been added/deleted.
185+
// compare to the indexed/persisted workflow config
186+
useEffect(() => {
187+
setUnsavedSearchProcessors(
188+
!isEqual(
189+
props.uiConfig?.search?.enrichRequest?.processors,
190+
props.workflow?.ui_metadata?.config?.search?.enrichRequest?.processors
191+
) ||
192+
!isEqual(
193+
props.uiConfig?.search?.enrichResponse?.processors,
194+
props.workflow?.ui_metadata?.config?.search?.enrichResponse
195+
?.processors
196+
)
197+
);
198+
}, [
199+
props.uiConfig?.search?.enrichRequest?.processors?.length,
200+
props.uiConfig?.search?.enrichResponse?.processors?.length,
201+
]);
162202

163203
// fetch the total template nodes
164204
useEffect(() => {
@@ -234,75 +274,64 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
234274
formGeneratedSearchTemplateNodes,
235275
]);
236276

237-
// Auto-save the UI metadata when users update form values.
238-
// Only update the underlying workflow template (deprovision/provision) when
239-
// users explicitly run ingest/search and need to have updated resources
240-
// to test against.
241-
// We use useCallback() with an autosave flag that is only set within the fn itself.
242-
// This is so we can fetch the latest values (uiConfig, formik values) inside a memoized fn,
243-
// but only when we need to.
244-
const [autosave, setAutosave] = useState<boolean>(false);
245-
function triggerAutosave(): void {
246-
setAutosave(!autosave);
247-
}
248-
const debounceAutosave = useCallback(
249-
debounce(async () => {
250-
triggerAutosave();
251-
}, 1000),
252-
[autosave]
253-
);
254-
255-
// Hook to execute autosave when triggered. Runs the update API with update_fields set to true,
256-
// to update the ui_metadata without updating the underlying template for a provisioned workflow.
257277
useEffect(() => {
258-
(async () => {
259-
if (!isEmpty(touched)) {
260-
const updatedTemplate = {
261-
name: props.workflow?.name,
262-
ui_metadata: {
263-
...props.workflow?.ui_metadata,
264-
config: formikToUiConfig(values, props.uiConfig as WorkflowConfig),
265-
},
266-
} as WorkflowTemplate;
267-
await dispatch(
268-
updateWorkflow({
269-
apiBody: {
278+
setIngestProvisioned(hasProvisionedIngestResources(props.workflow));
279+
}, [props.workflow]);
280+
281+
// Utility fn to update the workflow UI config only. A get workflow API call is subsequently run
282+
// to fetch the updated state.
283+
async function updateWorkflowUiConfig() {
284+
setIsRunningSave(true);
285+
const updatedTemplate = {
286+
name: props.workflow?.name,
287+
ui_metadata: {
288+
...props.workflow?.ui_metadata,
289+
config: formikToUiConfig(values, props.uiConfig as WorkflowConfig),
290+
},
291+
} as WorkflowTemplate;
292+
await dispatch(
293+
updateWorkflow({
294+
apiBody: {
295+
workflowId: props.workflow?.id as string,
296+
workflowTemplate: updatedTemplate,
297+
updateFields: true,
298+
reprovision: false,
299+
},
300+
dataSourceId,
301+
})
302+
)
303+
.unwrap()
304+
.then(async (result) => {
305+
setUnsavedIngestProcessors(false);
306+
setUnsavedSearchProcessors(false);
307+
setTouched({});
308+
new Promise((f) => setTimeout(f, 1000)).then(async () => {
309+
dispatch(
310+
getWorkflow({
270311
workflowId: props.workflow?.id as string,
271-
workflowTemplate: updatedTemplate,
272-
updateFields: true,
273-
reprovision: false,
274-
},
275-
dataSourceId,
276-
})
277-
)
278-
.unwrap()
279-
.then(async (result) => {
280-
// TODO: figure out clean way to update the "last updated"
281-
// section. The problem with re-fetching this every time, is it
282-
// triggers lots of component rebuilds due to the base workflow prop
283-
// changing.
284-
// get any updates after autosave
285-
// new Promise((f) => setTimeout(f, 1000)).then(async () => {
286-
// dispatch(getWorkflow(props.workflow?.id as string));
287-
// });
288-
})
289-
.catch((error: any) => {
290-
console.error('Error autosaving workflow: ', error);
291-
});
292-
}
293-
})();
294-
}, [autosave]);
312+
dataSourceId,
313+
})
314+
);
315+
});
316+
})
317+
.catch((error: any) => {
318+
console.error('Error saving workflow: ', error);
319+
})
320+
.finally(() => {
321+
setIsRunningSave(false);
322+
});
323+
}
295324

296-
// Hook to listen for changes to form values and trigger autosave
297-
useEffect(() => {
298-
if (!isEmpty(values)) {
299-
debounceAutosave();
325+
// Utility fn to revert any unsaved changes, reset the form
326+
function revertUnsavedChanges(): void {
327+
resetForm();
328+
if (
329+
(unsavedIngestProcessors || unsavedSearchProcessors) &&
330+
props.workflow?.ui_metadata?.config !== undefined
331+
) {
332+
props.setUiConfig(props.workflow?.ui_metadata?.config);
300333
}
301-
}, [values]);
302-
303-
useEffect(() => {
304-
setIngestProvisioned(hasProvisionedIngestResources(props.workflow));
305-
}, [props.workflow]);
334+
}
306335

307336
// Utility fn to update the workflow, including any updated/new resources.
308337
// The reprovision param is used to determine whether we are doing full
@@ -327,6 +356,8 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
327356
.unwrap()
328357
.then(async (result) => {
329358
await sleep(1000);
359+
setUnsavedIngestProcessors(false);
360+
setUnsavedSearchProcessors(false);
330361
success = true;
331362
// Kicking off an async task to re-fetch the workflow details
332363
// after some amount of time. Provisioning will finish in an indeterminate
@@ -370,6 +401,8 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
370401
.unwrap()
371402
.then(async (result) => {
372403
await sleep(1000);
404+
setUnsavedIngestProcessors(false);
405+
setUnsavedSearchProcessors(false);
373406
await dispatch(
374407
provisionWorkflow({
375408
workflowId: updatedWorkflow.id as string,
@@ -431,11 +464,10 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
431464
...(includeSearch && search !== undefined ? { search } : {}),
432465
};
433466
if (Object.keys(relevantValidationResults).length > 0) {
434-
// TODO: may want to persist more fine-grained form validation (ingest vs. search)
435-
// For example, running an ingest should be possible, even with some
436-
// invalid query or search processor config. And vice versa.
467+
getCore().notifications.toasts.addDanger('Missing or invalid fields');
437468
console.error('Form invalid');
438469
} else {
470+
setTouched({});
439471
const updatedConfig = formikToUiConfig(
440472
values,
441473
props.uiConfig as WorkflowConfig
@@ -623,7 +655,6 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
623655
.unwrap()
624656
.then(async (result) => {
625657
setFieldValue('ingest.enabled', false);
626-
await validateAndUpdateWorkflow(false);
627658
// @ts-ignore
628659
await dispatch(
629660
getWorkflow({
@@ -759,6 +790,41 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
759790
</EuiFlexItem>
760791
) : onIngest ? (
761792
<>
793+
<EuiFlexItem grow={false}>
794+
<EuiSmallButtonIcon
795+
iconType="editorUndo"
796+
aria-label="undo changes"
797+
isDisabled={
798+
isRunningSave || isRunningIngest
799+
? true
800+
: unsavedIngestProcessors
801+
? false
802+
: isEmpty(touched?.ingest?.enrich) &&
803+
isEmpty(touched?.ingest?.index)
804+
}
805+
onClick={() => {
806+
revertUnsavedChanges();
807+
}}
808+
/>
809+
</EuiFlexItem>
810+
<EuiFlexItem grow={false}>
811+
<EuiSmallButtonEmpty
812+
disabled={
813+
isRunningSave || isRunningIngest
814+
? true
815+
: unsavedIngestProcessors
816+
? false
817+
: isEmpty(touched?.ingest?.enrich) &&
818+
isEmpty(touched?.ingest?.index)
819+
}
820+
isLoading={isRunningSave}
821+
onClick={() => {
822+
updateWorkflowUiConfig();
823+
}}
824+
>
825+
{`Save`}
826+
</EuiSmallButtonEmpty>
827+
</EuiFlexItem>
762828
<EuiFlexItem grow={false}>
763829
<EuiSmallButton
764830
fill={false}
@@ -768,7 +834,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
768834
disabled={!ingestTemplatesDifferent}
769835
isLoading={isRunningIngest}
770836
>
771-
Run ingestion
837+
Build and run ingestion
772838
</EuiSmallButton>
773839
</EuiFlexItem>
774840
<EuiFlexItem grow={false}>
@@ -798,6 +864,39 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
798864
Back
799865
</EuiSmallButtonEmpty>
800866
</EuiFlexItem>
867+
<EuiFlexItem grow={false}>
868+
<EuiSmallButtonIcon
869+
iconType="editorUndo"
870+
aria-label="undo changes"
871+
isDisabled={
872+
isRunningSave || isRunningSearch
873+
? true
874+
: unsavedSearchProcessors
875+
? false
876+
: isEmpty(touched?.search)
877+
}
878+
onClick={() => {
879+
revertUnsavedChanges();
880+
}}
881+
/>
882+
</EuiFlexItem>
883+
<EuiFlexItem grow={false}>
884+
<EuiSmallButtonEmpty
885+
disabled={
886+
isRunningSave || isRunningSearch
887+
? true
888+
: unsavedSearchProcessors
889+
? false
890+
: isEmpty(touched?.search)
891+
}
892+
isLoading={isRunningSave}
893+
onClick={() => {
894+
updateWorkflowUiConfig();
895+
}}
896+
>
897+
{`Save`}
898+
</EuiSmallButtonEmpty>
899+
</EuiFlexItem>
801900
<EuiFlexItem grow={false}>
802901
<EuiSmallButton
803902
disabled={
@@ -811,7 +910,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
811910
validateAndRunQuery();
812911
}}
813912
>
814-
Run query
913+
Build and run query
815914
</EuiSmallButton>
816915
</EuiFlexItem>
817916
</>

0 commit comments

Comments
 (0)