darabos commited on
Commit
f98d0ad
·
1 Parent(s): a66000a

Wrap backend changes in transaction. Put callbacks in useCallback. Can add/delete edges now!

Browse files
Files changed (2) hide show
  1. server/crdt.py +8 -7
  2. web/src/workspace/Workspace.tsx +41 -39
server/crdt.py CHANGED
@@ -141,7 +141,7 @@ async def workspace_changed(name, changes, ws_crdt):
141
  if name in delayed_executions:
142
  delayed_executions[name].cancel()
143
  delay = min(
144
- change.keys.get("__execution_delay", {}).get("newValue", 0)
145
  for change in changes
146
  )
147
  if delay:
@@ -160,12 +160,13 @@ async def execute(ws_crdt, ws_pyd, delay=0):
160
  except asyncio.CancelledError:
161
  return
162
  await workspace.execute(ws_pyd)
163
- for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
164
- if "data" not in nc:
165
- nc["data"] = pycrdt.Map()
166
- # Display is added as an opaque Box.
167
- nc["data"]["display"] = np.data.display
168
- nc["data"]["error"] = np.data.error
 
169
 
170
 
171
  @contextlib.asynccontextmanager
 
141
  if name in delayed_executions:
142
  delayed_executions[name].cancel()
143
  delay = min(
144
+ getattr(change, "keys", {}).get("__execution_delay", {}).get("newValue", 0)
145
  for change in changes
146
  )
147
  if delay:
 
160
  except asyncio.CancelledError:
161
  return
162
  await workspace.execute(ws_pyd)
163
+ with ws_crdt.doc.transaction():
164
+ for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
165
+ if "data" not in nc:
166
+ nc["data"] = pycrdt.Map()
167
+ # Display is added as an opaque Box.
168
+ nc["data"]["display"] = np.data.display
169
+ nc["data"]["error"] = np.data.error
170
 
171
 
172
  @contextlib.asynccontextmanager
web/src/workspace/Workspace.tsx CHANGED
@@ -84,7 +84,7 @@ function LynxKiteFlow() {
84
  }
85
  }, [path]);
86
 
87
- const onNodesChange = (changes: any[]) => {
88
  // An update from the UI. Apply it to the local state...
89
  setNodes((nds) => applyNodeChanges(changes, nds));
90
  // ...and to the CRDT state. (Which could be the same, except for ReactFlow's internal copies.)
@@ -118,22 +118,39 @@ function LynxKiteFlow() {
118
  console.log('Unknown node change', ch);
119
  }
120
  }
121
- };
122
- const onEdgesChange = (changes: any[]) => {
123
  setEdges((eds) => applyEdgeChanges(changes, eds));
124
- };
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  const fetcher: Fetcher<Catalogs> = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
127
  const catalog = useSWR('/api/catalog', fetcher);
128
-
 
 
 
 
129
  const nodeTypes = useMemo(() => ({
130
  basic: NodeWithParams,
131
  table_view: NodeWithParams,
132
  }), []);
133
- function closeNodeSearch() {
134
  setNodeSearchSettings(undefined);
135
- }
136
- function toggleNodeSearch(event: MouseEvent) {
 
 
137
  if (nodeSearchSettings) {
138
  closeNodeSearch();
139
  return;
@@ -143,8 +160,8 @@ function LynxKiteFlow() {
143
  pos: { x: event.clientX, y: event.clientY },
144
  boxes: catalog.data![state.workspace.env!],
145
  });
146
- }
147
- function addNode(meta: OpsOp) {
148
  const node: Partial<WorkspaceNode> = {
149
  type: meta.type,
150
  data: {
@@ -167,35 +184,21 @@ function LynxKiteFlow() {
167
  wnodes.push(node as WorkspaceNode);
168
  setNodes([...nodes, node as WorkspaceNode]);
169
  closeNodeSearch();
170
- }
171
- const [nodeSearchSettings, setNodeSearchSettings] = useState(undefined as {
172
- pos: XYPosition,
173
- boxes: Catalog,
174
- } | undefined);
175
 
176
- function nodeClick(e: any) {
177
- const node = e.detail.node;
178
- const meta = node.data.meta;
179
- if (!meta) return;
180
- const sub_nodes = meta.sub_nodes;
181
- if (!sub_nodes) return;
182
- const event = e.detail.event;
183
- if (event.target.classList.contains('title')) return;
184
- setNodeSearchSettings({
185
- pos: { x: event.clientX, y: event.clientY },
186
- boxes: sub_nodes,
187
- });
188
- }
189
- function onConnect(params: Connection) {
190
- // const edge = {
191
- // id: `${params.source} ${params.target}`,
192
- // source: params.source,
193
- // sourceHandle: params.sourceHandle,
194
- // target: params.target,
195
- // targetHandle: params.targetHandle,
196
- // };
197
- // state.workspace.edges!.push(edge);
198
- }
199
  const parentDir = path!.split('/').slice(0, -1).join('/');
200
  return (
201
  <div className="workspace">
@@ -224,7 +227,6 @@ function LynxKiteFlow() {
224
  onNodesChange={onNodesChange}
225
  onEdgesChange={onEdgesChange}
226
  onPaneClick={toggleNodeSearch}
227
- onNodeClick={nodeClick}
228
  onConnect={onConnect}
229
  proOptions={{ hideAttribution: true }}
230
  maxZoom={3}
 
84
  }
85
  }, [path]);
86
 
87
+ const onNodesChange = useCallback((changes: any[]) => {
88
  // An update from the UI. Apply it to the local state...
89
  setNodes((nds) => applyNodeChanges(changes, nds));
90
  // ...and to the CRDT state. (Which could be the same, except for ReactFlow's internal copies.)
 
118
  console.log('Unknown node change', ch);
119
  }
120
  }
121
+ }, [state]);
122
+ const onEdgesChange = useCallback((changes: any[]) => {
123
  setEdges((eds) => applyEdgeChanges(changes, eds));
124
+ const wedges = state.workspace?.edges;
125
+ if (!wedges) return;
126
+ for (const ch of changes) {
127
+ console.log('edge change', ch);
128
+ const edgeIndex = wedges.findIndex((e) => e.id === ch.id);
129
+ if (ch.type === 'remove') {
130
+ wedges.splice(edgeIndex, 1);
131
+ } else {
132
+ console.log('Unknown edge change', ch);
133
+ }
134
+ }
135
+ }, [state]);
136
 
137
  const fetcher: Fetcher<Catalogs> = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
138
  const catalog = useSWR('/api/catalog', fetcher);
139
+ const [suppressSearchUntil, setSuppressSearchUntil] = useState(0);
140
+ const [nodeSearchSettings, setNodeSearchSettings] = useState(undefined as {
141
+ pos: XYPosition,
142
+ boxes: Catalog,
143
+ } | undefined);
144
  const nodeTypes = useMemo(() => ({
145
  basic: NodeWithParams,
146
  table_view: NodeWithParams,
147
  }), []);
148
+ const closeNodeSearch = useCallback(() => {
149
  setNodeSearchSettings(undefined);
150
+ setSuppressSearchUntil(Date.now() + 200);
151
+ }, [setNodeSearchSettings, setSuppressSearchUntil]);
152
+ const toggleNodeSearch = useCallback((event: MouseEvent) => {
153
+ if (suppressSearchUntil > Date.now()) return;
154
  if (nodeSearchSettings) {
155
  closeNodeSearch();
156
  return;
 
160
  pos: { x: event.clientX, y: event.clientY },
161
  boxes: catalog.data![state.workspace.env!],
162
  });
163
+ }, [setNodeSearchSettings, suppressSearchUntil]);
164
+ const addNode = useCallback((meta: OpsOp) => {
165
  const node: Partial<WorkspaceNode> = {
166
  type: meta.type,
167
  data: {
 
184
  wnodes.push(node as WorkspaceNode);
185
  setNodes([...nodes, node as WorkspaceNode]);
186
  closeNodeSearch();
187
+ }, [state, reactFlow, setNodes]);
 
 
 
 
188
 
189
+ const onConnect = useCallback((connection: Connection) => {
190
+ setSuppressSearchUntil(Date.now() + 200);
191
+ const edge = {
192
+ id: `${connection.source} ${connection.target}`,
193
+ source: connection.source,
194
+ sourceHandle: connection.sourceHandle!,
195
+ target: connection.target,
196
+ targetHandle: connection.targetHandle!,
197
+ };
198
+ console.log(JSON.stringify(edge));
199
+ state.workspace.edges!.push(edge);
200
+ setEdges((oldEdges) => [...oldEdges, edge]);
201
+ }, [state, setEdges]);
 
 
 
 
 
 
 
 
 
 
202
  const parentDir = path!.split('/').slice(0, -1).join('/');
203
  return (
204
  <div className="workspace">
 
227
  onNodesChange={onNodesChange}
228
  onEdgesChange={onEdgesChange}
229
  onPaneClick={toggleNodeSearch}
 
230
  onConnect={onConnect}
231
  proOptions={{ hideAttribution: true }}
232
  maxZoom={3}