3
3
* SPDX-License-Identifier: Apache-2.0
4
4
*/
5
5
6
- import React , { useCallback , useEffect , useState } from 'react' ;
6
+ import React , { useEffect , useState } from 'react' ;
7
7
import { getIn , useFormikContext } from 'formik' ;
8
- import { debounce , isEmpty , isEqual } from 'lodash' ;
8
+ import { isEmpty , isEqual } from 'lodash' ;
9
9
import {
10
10
EuiSmallButton ,
11
11
EuiSmallButtonEmpty ,
@@ -24,6 +24,7 @@ import {
24
24
EuiStepsHorizontal ,
25
25
EuiText ,
26
26
EuiTitle ,
27
+ EuiSmallButtonIcon ,
27
28
} from '@elastic/eui' ;
28
29
import {
29
30
MAX_WORKFLOW_NAME_TO_DISPLAY ,
@@ -96,14 +97,17 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
96
97
const {
97
98
submitForm,
98
99
validateForm,
100
+ resetForm,
99
101
setFieldValue,
102
+ setTouched,
100
103
values,
101
104
touched,
102
105
} = useFormikContext < WorkflowFormValues > ( ) ;
103
106
const dispatch = useAppDispatch ( ) ;
104
107
const dataSourceId = getDataSourceId ( ) ;
105
108
106
- // running ingest/search state
109
+ // transient running states
110
+ const [ isRunningSave , setIsRunningSave ] = useState < boolean > ( false ) ;
107
111
const [ isRunningIngest , setIsRunningIngest ] = useState < boolean > ( false ) ;
108
112
const [ isRunningSearch , setIsRunningSearch ] = useState < boolean > ( false ) ;
109
113
const [ isRunningDelete , setIsRunningDelete ] = useState < boolean > ( false ) ;
@@ -129,7 +133,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
129
133
isEmpty ( getIn ( values , 'search.enrichResponse' ) ) ;
130
134
131
135
// 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
133
137
// based on any discrepancies found.
134
138
const [ persistedTemplateNodes , setPersistedTemplateNodes ] = useState <
135
139
TemplateNode [ ]
@@ -159,6 +163,42 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
159
163
const [ searchTemplatesDifferent , setSearchTemplatesDifferent ] = useState <
160
164
boolean
161
165
> ( 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
+ ] ) ;
162
202
163
203
// fetch the total template nodes
164
204
useEffect ( ( ) => {
@@ -234,75 +274,64 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
234
274
formGeneratedSearchTemplateNodes ,
235
275
] ) ;
236
276
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.
257
277
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 ( {
270
311
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
+ }
295
324
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 ) ;
300
333
}
301
- } , [ values ] ) ;
302
-
303
- useEffect ( ( ) => {
304
- setIngestProvisioned ( hasProvisionedIngestResources ( props . workflow ) ) ;
305
- } , [ props . workflow ] ) ;
334
+ }
306
335
307
336
// Utility fn to update the workflow, including any updated/new resources.
308
337
// The reprovision param is used to determine whether we are doing full
@@ -327,6 +356,8 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
327
356
. unwrap ( )
328
357
. then ( async ( result ) => {
329
358
await sleep ( 1000 ) ;
359
+ setUnsavedIngestProcessors ( false ) ;
360
+ setUnsavedSearchProcessors ( false ) ;
330
361
success = true ;
331
362
// Kicking off an async task to re-fetch the workflow details
332
363
// after some amount of time. Provisioning will finish in an indeterminate
@@ -370,6 +401,8 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
370
401
. unwrap ( )
371
402
. then ( async ( result ) => {
372
403
await sleep ( 1000 ) ;
404
+ setUnsavedIngestProcessors ( false ) ;
405
+ setUnsavedSearchProcessors ( false ) ;
373
406
await dispatch (
374
407
provisionWorkflow ( {
375
408
workflowId : updatedWorkflow . id as string ,
@@ -431,11 +464,10 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
431
464
...( includeSearch && search !== undefined ? { search } : { } ) ,
432
465
} ;
433
466
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' ) ;
437
468
console . error ( 'Form invalid' ) ;
438
469
} else {
470
+ setTouched ( { } ) ;
439
471
const updatedConfig = formikToUiConfig (
440
472
values ,
441
473
props . uiConfig as WorkflowConfig
@@ -623,7 +655,6 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
623
655
. unwrap ( )
624
656
. then ( async ( result ) => {
625
657
setFieldValue ( 'ingest.enabled' , false ) ;
626
- await validateAndUpdateWorkflow ( false ) ;
627
658
// @ts -ignore
628
659
await dispatch (
629
660
getWorkflow ( {
@@ -759,6 +790,41 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
759
790
</ EuiFlexItem >
760
791
) : onIngest ? (
761
792
< >
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 >
762
828
< EuiFlexItem grow = { false } >
763
829
< EuiSmallButton
764
830
fill = { false }
@@ -768,7 +834,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
768
834
disabled = { ! ingestTemplatesDifferent }
769
835
isLoading = { isRunningIngest }
770
836
>
771
- Run ingestion
837
+ Build and run ingestion
772
838
</ EuiSmallButton >
773
839
</ EuiFlexItem >
774
840
< EuiFlexItem grow = { false } >
@@ -798,6 +864,39 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
798
864
Back
799
865
</ EuiSmallButtonEmpty >
800
866
</ 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 >
801
900
< EuiFlexItem grow = { false } >
802
901
< EuiSmallButton
803
902
disabled = {
@@ -811,7 +910,7 @@ export function WorkflowInputs(props: WorkflowInputsProps) {
811
910
validateAndRunQuery ( ) ;
812
911
} }
813
912
>
814
- Run query
913
+ Build and run query
815
914
</ EuiSmallButton >
816
915
</ EuiFlexItem >
817
916
</ >
0 commit comments