Skip to content

Commit 1156751

Browse files
authored
Merge pull request #55 from FlowTestAI/flow-state-store
Flow canvas state store
2 parents 0b74fc7 + 1778ecd commit 1156751

29 files changed

+866
-491
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"react-router": "^6.15.0",
3030
"react-router-dom": "^6.22.2",
3131
"react-scripts": "5.0.1",
32+
"react-toastify": "^10.0.5",
3233
"react-tooltip": "^5.26.2",
3334
"reactflow": "^11.8.3",
3435
"socket.io-client": "^4.7.4",

src/components/atoms/Tabs.js

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
22
import { XMarkIcon } from '@heroicons/react/24/outline';
33
import { useTabStore } from 'stores/TabStore';
44
import ConfirmActionModal from 'components/molecules/modals/ConfirmActionModal';
5+
import { isEqual } from 'lodash';
56

67
const Tabs = () => {
78
const tabs = useTabStore((state) => state.tabs);
@@ -12,28 +13,31 @@ const Tabs = () => {
1213
const closeTab = useTabStore((state) => state.closeTab);
1314
const [closingTabId, setClosingTabId] = useState('');
1415
const [closingCollectionId, setClosingCollectionId] = useState('');
16+
1517
const activeTabStyles =
1618
'before:absolute before:h-[0.25rem] before:w-full before:bg-slate-300 before:content-[""] before:bottom-0 before:left-0';
1719
const tabCommonStyles =
1820
'tab flex items-center gap-x-2 border-r border-neutral-300 bg-transparent pr-0 tracking-[0.15em] transition duration-500 ease-in text-sm';
1921
const messageForConfirmActionModal = 'You have unsaved changes in the flowtest, are you sure you want to close it?';
20-
const handleCloseTab = (event) => {
22+
23+
const handleCloseTab = (event, tab) => {
2124
event.stopPropagation();
2225
event.preventDefault();
23-
const tabId = event.currentTarget.dataset.tabId;
24-
const { isDirty, collectionId } = tabs.find((tab) => {
25-
if (tab.id === tabId) return tab;
26-
});
27-
setClosingTabId(tabId);
28-
setClosingCollectionId(collectionId);
26+
// const tabId = event.currentTarget.dataset.tabId;
27+
// const { isDirty, collectionId } = tabs.find((tab) => {
28+
// if (tab.id === tabId) return tab;
29+
// });
30+
setClosingTabId(tab.id);
31+
setClosingCollectionId(tab.collectionId);
2932

30-
if (isDirty) {
31-
console.debug(`Confirm close for tabId: ${tabId} : collectionId: ${collectionId}`);
33+
if (tab.flowDataDraft && !isEqual(tab.flowData, tab.flowDataDraft)) {
34+
console.debug(`Confirm close for tabId: ${tab.id} : collectionId: ${tab.collectionId}`);
3235
setConfirmActionModalOpen(true);
3336
return;
3437
}
35-
closeTab(tabId, collectionId);
38+
closeTab(tab.id, tab.collectionId);
3639
};
40+
3741
return (
3842
<div role='tablist' className='tabs tabs-lg'>
3943
{tabs
@@ -55,11 +59,11 @@ const Tabs = () => {
5559
<a>{tab.name}</a>
5660
{/* close needs to be a separate clickable component other wise it gets confused with above */}
5761
<div
58-
className='flex items-center h-full px-2 hover:rounded hover:rounded-l-none hover:bg-slate-200'
62+
className='flex h-full items-center px-2 hover:rounded hover:rounded-l-none hover:bg-slate-200'
5963
data-tab-id={tab.id}
60-
onClick={handleCloseTab}
64+
onClick={(e) => handleCloseTab(e, tab)}
6165
>
62-
<XMarkIcon className='w-4 h-4' />
66+
<XMarkIcon className='h-4 w-4' />
6367
</div>
6468
</div>
6569
);

src/components/molecules/flow/graph/Graph.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// assumption is that apis are giving json as output
22

3+
import useCanvasStore from 'stores/CanvasStore';
34
import useCollectionStore from 'stores/CollectionStore';
45
import { useTabStore } from 'stores/TabStore';
56
import { computeAuthNode } from './compute/authnode';
@@ -69,7 +70,7 @@ class Graph {
6970

7071
if (node.type === 'outputNode') {
7172
this.logs.push(`Output: ${JSON.stringify(prevNodeOutputData)}`);
72-
node.data.setOutput(prevNodeOutputData);
73+
useCanvasStore.getState().setOutputNode(node.id, prevNodeOutputData);
7374
result = ['Success', node, prevNodeOutput];
7475
}
7576

@@ -94,7 +95,7 @@ class Graph {
9495
}
9596

9697
if (node.type === 'authNode') {
97-
this.auth = computeAuthNode(node.data.auth, this.env);
98+
this.auth = node.data.type ? computeAuthNode(node.data, this.env) : undefined;
9899
result = ['Success', node, prevNodeOutput];
99100
}
100101

@@ -134,7 +135,7 @@ class Graph {
134135
// reset every output node for a fresh run
135136
this.nodes.forEach((node) => {
136137
if (node.type === 'outputNode') {
137-
node.data.setOutput(undefined);
138+
useCanvasStore.getState().unSetOutputNode(node.id);
138139
}
139140
});
140141
this.graphRunNodeOutput = {};

src/components/molecules/flow/graph/compute/requestnode.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const formulateRequest = (node, finalUrl, variablesDict, auth, logs) => {
3838
data: requestData,
3939
};
4040

41-
if (auth.type === 'basic-auth') {
41+
if (auth && auth.type === 'basic-auth') {
4242
options.auth = {};
4343
options.auth.username = auth.username;
4444
options.auth.password = auth.password;

src/components/molecules/flow/index.js

Lines changed: 20 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { PropTypes } from 'prop-types';
33
import ReactFlow, { useNodesState, useEdgesState, addEdge, Controls, Background, ControlButton } from 'reactflow';
44
import 'reactflow/dist/style.css';
55
import { cloneDeep, isEqual } from 'lodash';
6+
import { toast } from 'react-toastify';
67

78
// css
89
import './index.css';
@@ -27,12 +28,15 @@ import FlowNode from 'components/atoms/flow/FlowNode';
2728
import { Popover } from '@headlessui/react';
2829
import { generateFlowData } from './flowtestai';
2930
import { GENAI_MODELS } from 'constants/Common';
31+
import useCanvasStore from 'stores/CanvasStore';
32+
33+
import { shallow } from 'zustand/shallow';
3034

3135
const StartNode = () => (
3236
<FlowNode title='Start' handleLeft={false} handleRight={true} handleRightData={{ type: 'source' }}></FlowNode>
3337
);
3438

35-
const init = (flowData) => {
39+
export const init = (flowData) => {
3640
// Initialization
3741
if (flowData && flowData.nodes && flowData.edges) {
3842
return {
@@ -95,36 +99,19 @@ const init = (flowData) => {
9599
}
96100
};
97101

98-
const Flow = ({ tabId, collectionId, flowData }) => {
99-
useEffect(() => {
100-
// Action to perform on tab change
101-
console.log(`Tab changed to: ${tabId}`);
102-
console.log(flowData);
103-
// perform actions based on the new tabId
104-
const result = init(cloneDeep(flowData));
105-
setNodes(result.nodes);
106-
setEdges(result.edges);
107-
}, [tabId]);
102+
const selector = (state) => ({
103+
nodes: state.nodes,
104+
edges: state.edges,
105+
onNodesChange: state.onNodesChange,
106+
onEdgesChange: state.onEdgesChange,
107+
onConnect: state.onConnect,
108+
setNodes: state.setNodes,
109+
setEdges: state.setEdges,
110+
});
108111

109-
const setCanvasDirty = () => {
110-
console.debug('set canvas dirty');
111-
const tab = useTabStore.getState().tabs.find((t) => t.id === tabId);
112-
if (tab) {
113-
tab.isDirty = true;
114-
tab.flowData = {
115-
nodes: nodes.map((node) => {
116-
const _node = JSON.parse(JSON.stringify(node));
117-
return { ..._node };
118-
}),
119-
edges: edges.map((edge) => {
120-
return {
121-
...edge,
122-
animated: false,
123-
};
124-
}),
125-
};
126-
}
127-
};
112+
const Flow = ({ collectionId }) => {
113+
const { nodes, edges, onNodesChange, onEdgesChange, onConnect, setNodes, setEdges } = useCanvasStore(selector);
114+
//console.log(nodes);
128115

129116
// notification
130117
// eslint-disable-next-line no-unused-vars
@@ -151,29 +138,6 @@ const Flow = ({ tabId, collectionId, flowData }) => {
151138
[],
152139
);
153140

154-
const [nodes, setNodes, onNodesChange] = useNodesState([]);
155-
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
156-
157-
useEffect(() => {
158-
// skip inital render
159-
if (flowData === undefined || (isEqual(nodes, []) && isEqual(edges, []))) {
160-
return;
161-
}
162-
if (flowData && isEqual(JSON.parse(JSON.stringify(nodes)), flowData.nodes) && isEqual(edges, flowData.edges)) {
163-
console.debug('canvas is unchanged');
164-
return;
165-
}
166-
setCanvasDirty();
167-
}, [nodes, edges]);
168-
169-
const onConnect = (params) => {
170-
const newEdge = {
171-
...params,
172-
type: 'buttonedge',
173-
};
174-
setEdges((eds) => addEdge(newEdge, eds));
175-
};
176-
177141
const runnableEdges = (runnable) => {
178142
const updatedEdges = reactFlowInstance.getEdges().map((edge) => {
179143
return {
@@ -220,7 +184,7 @@ const Flow = ({ tabId, collectionId, flowData }) => {
220184
};
221185
console.debug('Dropped node: ', newNode);
222186

223-
setNodes((nds) => nds.concat(newNode));
187+
setNodes([...useCanvasStore.getState().nodes, newNode]);
224188
},
225189
[reactFlowInstance],
226190
);
@@ -279,7 +243,7 @@ const Flow = ({ tabId, collectionId, flowData }) => {
279243
onInit={setReactFlowInstance}
280244
onDrop={onDrop}
281245
onDragOver={onDragOver}
282-
onNodeDragStop={() => setCanvasDirty()}
246+
//onNodeDragStop={() => setCanvasDirty()}
283247
isValidConnection={isValidConnection}
284248
//fitView
285249
>
@@ -313,8 +277,8 @@ const Flow = ({ tabId, collectionId, flowData }) => {
313277
setEdges(result.edges);
314278
})
315279
.catch((error) => {
316-
// TODO: show error in UI
317280
console.log(error);
281+
toast.error(`Error while generating flow data`);
318282
});
319283
}}
320284
>
@@ -328,9 +292,7 @@ const Flow = ({ tabId, collectionId, flowData }) => {
328292
};
329293

330294
Flow.propTypes = {
331-
tabId: PropTypes.string.isRequired,
332295
collectionId: PropTypes.string.isRequired,
333-
flowData: PropTypes.object.isRequired,
334296
};
335297

336298
export default Flow;

src/components/molecules/flow/nodes/AuthNode.js

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,18 @@
11
import * as React from 'react';
22
import { PropTypes } from 'prop-types';
33
import FlowNode from 'components/atoms/flow/FlowNode';
4+
import useCanvasStore from 'stores/CanvasStore';
45

5-
const AuthNode = ({ data }) => {
6-
/* its better to have no space strings as values since they are less error/bug prone */
7-
const initState = () => {
8-
if (data.auth && data.auth.type) {
9-
return data.auth.type; // should return basic-auth
10-
} else {
11-
data.auth = {};
12-
data.auth.type = 'no-auth';
13-
return 'no-auth';
14-
}
15-
};
16-
17-
// const [anchorEl, setAnchorEl] = React.useState(null);
18-
const [auth, setAuth] = React.useState(initState());
6+
const AuthNode = ({ id, data }) => {
7+
const setAuthNodeType = useCanvasStore((state) => state.setAuthNodeType);
8+
const setBasicAuthValues = useCanvasStore((state) => state.setBasicAuthValues);
199

20-
/**
21-
* Not sure whether you need to set the value for data props/param or not.
22-
* Technically you should not set the value for a prop/param in a component
23-
* Check this and other places in this file once
24-
*/
2510
const handleChange = (value, option) => {
26-
data.auth[option] = value;
11+
setBasicAuthValues(id, option, value);
2712
};
2813

2914
const handleSelection = (event) => {
30-
const selectedValue = event.target?.value;
31-
setAuth(selectedValue);
32-
data.auth = {};
33-
data.auth.type = selectedValue;
15+
setAuthNodeType(id, event.target?.value);
3416
};
3517

3618
return (
@@ -47,25 +29,27 @@ const AuthNode = ({ data }) => {
4729
<select
4830
onChange={handleSelection}
4931
name='auth-type'
50-
value={auth}
51-
className='w-full px-1 py-2 border rounded-lg border-neutral-500 text-neutral-500 outline-0 focus:ring-0'
32+
value={data.type ? data.type : 'no-auth'}
33+
className='w-full rounded-lg border border-neutral-500 px-1 py-2 text-neutral-500 outline-0 focus:ring-0'
5234
>
5335
<option value='no-auth'>No Auth</option>
5436
<option value='basic-auth'>Basic Auth</option>
5537
</select>
5638
</div>
57-
{auth === 'basic-auth' && (
39+
{data.type === 'basic-auth' && (
5840
<div>
5941
<input
6042
type='text'
61-
placeholder={data.auth.username ? data.auth.username : 'Username'}
43+
placeholder='Username'
44+
value={data.username ? data.username : ''}
6245
className='nodrag nowheel mb-2 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 outline-blue-300 focus:border-blue-100 focus:ring-blue-100'
6346
name='username'
6447
onChange={(e) => handleChange(e.target.value, 'username')}
6548
/>
6649
<input
6750
type='text'
68-
placeholder={data.auth.password ? data.auth.password : 'Password'}
51+
placeholder='Password'
52+
value={data.password ? data.password : ''}
6953
className='nodrag nowheel block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 outline-blue-300 focus:border-blue-100 focus:ring-blue-100'
7054
name='password'
7155
onChange={(e) => handleChange(e.target.value, 'password')}

src/components/molecules/flow/nodes/DelayNode.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import * as React from 'react';
22
import { PropTypes } from 'prop-types';
33
import FlowNode from 'components/atoms/flow/FlowNode';
4+
import useCanvasStore from 'stores/CanvasStore';
45

5-
/**
6-
* I have commented the code which is not required but do check it once
7-
*/
8-
const DelayNode = ({ data }) => {
9-
const [value, setValue] = React.useState(data.delay ? data.delay : 0);
10-
data.delay = value;
6+
const DelayNode = ({ id, data }) => {
7+
const setDelayNodeValue = useCanvasStore((state) => state.setDelayNodeValue);
118

129
/**
1310
* ToDo: Implement Debouncing for this function
1411
*/
1512
const handleDelayInMsInputChange = (event) => {
1613
event.preventDefault();
17-
const delayInMs = event.target.value;
18-
setValue(delayInMs);
14+
setDelayNodeValue(id, event.target.value);
1915
};
2016

2117
return (

0 commit comments

Comments
 (0)