4
4
*/
5
5
6
6
import React , { useRef , useState , useEffect , useContext } from 'react' ;
7
+ import { useDispatch , useSelector } from 'react-redux' ;
7
8
import { useOnSelectionChange } from 'reactflow' ;
8
9
import { Form , Formik } from 'formik' ;
9
10
import * as yup from 'yup' ;
10
11
import { cloneDeep } from 'lodash' ;
11
- import { EuiButton , EuiResizableContainer } from '@elastic/eui' ;
12
+ import {
13
+ EuiButton ,
14
+ EuiCallOut ,
15
+ EuiPageHeader ,
16
+ EuiResizableContainer ,
17
+ } from '@elastic/eui' ;
12
18
import {
13
19
Workflow ,
14
20
WorkspaceFormValues ,
@@ -17,12 +23,18 @@ import {
17
23
WorkspaceSchemaObj ,
18
24
componentDataToFormik ,
19
25
getComponentSchema ,
26
+ toWorkspaceFlow ,
27
+ validateWorkspaceFlow ,
28
+ WorkspaceFlowState ,
29
+ toTemplateFlows ,
20
30
} from '../../../../common' ;
21
- import { rfContext } from '../../../store' ;
31
+ import { AppState , removeDirty , setDirty , rfContext } from '../../../store' ;
22
32
import { Workspace } from './workspace' ;
23
33
import { ComponentDetails } from '../component_details' ;
34
+ import { processNodes , saveWorkflow } from '../utils' ;
24
35
25
36
interface ResizableWorkspaceProps {
37
+ isNewWorkflow : boolean ;
26
38
workflow ?: Workflow ;
27
39
}
28
40
@@ -33,6 +45,27 @@ const COMPONENT_DETAILS_PANEL_ID = 'component_details_panel_id';
33
45
* panels - the ReactFlow workspace panel and the selected component details panel.
34
46
*/
35
47
export function ResizableWorkspace ( props : ResizableWorkspaceProps ) {
48
+ const dispatch = useDispatch ( ) ;
49
+
50
+ // Overall workspace state
51
+ const isDirty = useSelector ( ( state : AppState ) => state . workspace . isDirty ) ;
52
+ const [ isFirstSave , setIsFirstSave ] = useState < boolean > ( props . isNewWorkflow ) ;
53
+ const isSaveable = isFirstSave ? true : isDirty ;
54
+
55
+ // Workflow state
56
+ const [ workflow , setWorkflow ] = useState < Workflow | undefined > (
57
+ props . workflow
58
+ ) ;
59
+
60
+ // Formik form state
61
+ const [ formValues , setFormValues ] = useState < WorkspaceFormValues > ( { } ) ;
62
+ const [ formSchema , setFormSchema ] = useState < WorkspaceSchema > ( yup . object ( { } ) ) ;
63
+
64
+ // Validation states. Maintain separate state for form vs. overall flow so
65
+ // we can have fine-grained errors and action items for users
66
+ const [ formValidOnSubmit , setFormValidOnSubmit ] = useState < boolean > ( true ) ;
67
+ const [ flowValidOnSubmit , setFlowValidOnSubmit ] = useState < boolean > ( true ) ;
68
+
36
69
// Component details side panel state
37
70
const [ isDetailsPanelOpen , setisDetailsPanelOpen ] = useState < boolean > ( true ) ;
38
71
const collapseFn = useRef (
@@ -68,6 +101,25 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
68
101
} ,
69
102
} ) ;
70
103
104
+ // Hook to update the workflow's flow state, if applicable. It may not exist if
105
+ // it is a backend-only-created workflow, or a new, unsaved workflow. If so,
106
+ // generate a default one based on the 'workflows' JSON field.
107
+ useEffect ( ( ) => {
108
+ const workflowCopy = { ...props . workflow } as Workflow ;
109
+ if ( workflowCopy ) {
110
+ if ( ! workflowCopy . workspaceFlowState ) {
111
+ workflowCopy . workspaceFlowState = toWorkspaceFlow (
112
+ workflowCopy . workflows
113
+ ) ;
114
+ console . debug (
115
+ `There is no saved UI flow for workflow: ${ workflowCopy . name } . Generating a default one.`
116
+ ) ;
117
+ }
118
+ setWorkflow ( workflowCopy ) ;
119
+ }
120
+ } , [ props . workflow ] ) ;
121
+
122
+ // Hook to updated the selected ReactFlow component
71
123
useEffect ( ( ) => {
72
124
reactFlowInstance ?. setNodes ( ( nodes : ReactFlowComponent [ ] ) =>
73
125
nodes . map ( ( node ) => {
@@ -80,24 +132,20 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
80
132
) ;
81
133
} , [ selectedComponent ] ) ;
82
134
83
- // Formik form state
84
- const [ formValues , setFormValues ] = useState < WorkspaceFormValues > ( { } ) ;
85
- const [ formSchema , setFormSchema ] = useState < WorkspaceSchema > ( yup . object ( { } ) ) ;
86
-
87
135
// Initialize the form state to an existing workflow, if applicable.
88
136
useEffect ( ( ) => {
89
- if ( props . workflow ?. workspaceFlowState ) {
137
+ if ( workflow ?. workspaceFlowState ) {
90
138
const initFormValues = { } as WorkspaceFormValues ;
91
139
const initSchemaObj = { } as WorkspaceSchemaObj ;
92
- props . workflow . workspaceFlowState . nodes . forEach ( ( node ) => {
140
+ workflow . workspaceFlowState . nodes . forEach ( ( node ) => {
93
141
initFormValues [ node . id ] = componentDataToFormik ( node . data ) ;
94
142
initSchemaObj [ node . id ] = getComponentSchema ( node . data ) ;
95
143
} ) ;
96
144
const initFormSchema = yup . object ( initSchemaObj ) as WorkspaceSchema ;
97
145
setFormValues ( initFormValues ) ;
98
146
setFormSchema ( initFormSchema ) ;
99
147
}
100
- } , [ props . workflow ] ) ;
148
+ } , [ workflow ] ) ;
101
149
102
150
// Update the form values and validation schema when a node is added
103
151
// or removed from the workspace.
@@ -129,6 +177,16 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
129
177
setFormSchema ( updatedSchema ) ;
130
178
}
131
179
180
+ /**
181
+ * Function to pass down to the Formik <Form> components as a listener to propagate
182
+ * form changes to this parent component to re-enable save button, etc.
183
+ */
184
+ function onFormChange ( ) {
185
+ if ( ! isDirty ) {
186
+ dispatch ( setDirty ( ) ) ;
187
+ }
188
+ }
189
+
132
190
return (
133
191
< Formik
134
192
enableReinitialize = { true }
@@ -139,9 +197,84 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
139
197
>
140
198
{ ( formikProps ) => (
141
199
< Form >
200
+ { ! formValidOnSubmit && (
201
+ < EuiCallOut
202
+ title = "There are empty or invalid fields"
203
+ color = "danger"
204
+ iconType = "alert"
205
+ style = { { marginBottom : '16px' } }
206
+ >
207
+ Please address the highlighted fields and try saving again.
208
+ </ EuiCallOut >
209
+ ) }
210
+ { ! flowValidOnSubmit && (
211
+ < EuiCallOut
212
+ title = "The configured flow is invalid"
213
+ color = "danger"
214
+ iconType = "alert"
215
+ style = { { marginBottom : '16px' } }
216
+ >
217
+ Please ensure there are no open connections between the
218
+ components.
219
+ </ EuiCallOut >
220
+ ) }
221
+ < EuiPageHeader
222
+ style = { { marginBottom : '8px' } }
223
+ rightSideItems = { [
224
+ // TODO: add launch logic
225
+ < EuiButton fill = { false } onClick = { ( ) => { } } >
226
+ Launch
227
+ </ EuiButton > ,
228
+ < EuiButton
229
+ fill = { false }
230
+ disabled = { ! isSaveable }
231
+ // TODO: if props.isNewWorkflow is true, clear the workflow cache if saving is successful.
232
+ onClick = { ( ) => {
233
+ dispatch ( removeDirty ( ) ) ;
234
+ if ( isFirstSave ) {
235
+ setIsFirstSave ( false ) ;
236
+ }
237
+ // Submit the form to bubble up any errors.
238
+ // Ideally we handle Promise accept/rejects with submitForm(), but there is
239
+ // open issues for that - see https://github.com/jaredpalmer/formik/issues/2057
240
+ // The workaround is to additionally execute validateForm() which will return any errors found.
241
+ formikProps . submitForm ( ) ;
242
+ formikProps . validateForm ( ) . then ( ( validationResults : { } ) => {
243
+ if ( Object . keys ( validationResults ) . length > 0 ) {
244
+ setFormValidOnSubmit ( false ) ;
245
+ } else {
246
+ setFormValidOnSubmit ( true ) ;
247
+ // @ts -ignore
248
+ let curFlowState = reactFlowInstance . toObject ( ) as WorkspaceFlowState ;
249
+ curFlowState = {
250
+ ...curFlowState ,
251
+ nodes : processNodes ( curFlowState . nodes ) ,
252
+ } ;
253
+ if ( validateWorkspaceFlow ( curFlowState ) ) {
254
+ setFlowValidOnSubmit ( true ) ;
255
+ const updatedWorkflow = {
256
+ ...workflow ,
257
+ workspaceFlowState : curFlowState ,
258
+ workflows : toTemplateFlows ( curFlowState ) ,
259
+ } as Workflow ;
260
+ saveWorkflow ( updatedWorkflow ) ;
261
+ } else {
262
+ setFlowValidOnSubmit ( false ) ;
263
+ }
264
+ }
265
+ } ) ;
266
+ } }
267
+ >
268
+ Save
269
+ </ EuiButton > ,
270
+ ] }
271
+ bottomBorder = { false }
272
+ />
142
273
< EuiResizableContainer
143
274
direction = "horizontal"
144
- style = { { marginLeft : '-8px' , marginTop : '-8px' } }
275
+ style = { {
276
+ marginLeft : '-8px' ,
277
+ } }
145
278
>
146
279
{ ( EuiResizablePanel , EuiResizableButton , { togglePanel } ) => {
147
280
if ( togglePanel ) {
@@ -153,33 +286,34 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
153
286
< >
154
287
< EuiResizablePanel
155
288
mode = "main"
156
- initialSize = { 75 }
289
+ initialSize = { 80 }
157
290
minSize = "50%"
158
291
paddingSize = "s"
159
292
>
160
293
< Workspace
161
- workflow = { props . workflow }
294
+ workflow = { workflow }
162
295
onNodesChange = { onNodesChange }
163
296
/>
164
297
</ EuiResizablePanel >
165
298
< EuiResizableButton />
166
299
< EuiResizablePanel
300
+ style = { { marginRight : '-16px' } }
167
301
id = { COMPONENT_DETAILS_PANEL_ID }
168
302
mode = "collapsible"
169
303
initialSize = { 25 }
170
304
minSize = "10%"
171
305
paddingSize = "s"
172
306
onToggleCollapsedInternal = { ( ) => onToggleChange ( ) }
173
307
>
174
- < ComponentDetails selectedComponent = { selectedComponent } />
308
+ < ComponentDetails
309
+ selectedComponent = { selectedComponent }
310
+ onFormChange = { onFormChange }
311
+ />
175
312
</ EuiResizablePanel >
176
313
</ >
177
314
) ;
178
315
} }
179
316
</ EuiResizableContainer >
180
- < EuiButton onClick = { ( ) => formikProps . handleSubmit ( ) } >
181
- Submit
182
- </ EuiButton >
183
317
</ Form >
184
318
) }
185
319
</ Formik >
0 commit comments