Spaces:
Running
Running
Wrap backend changes in transaction. Put callbacks in useCallback. Can add/delete edges now!
Browse files- server/crdt.py +8 -7
- 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
|
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 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
|
|
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 |
-
|
134 |
setNodeSearchSettings(undefined);
|
135 |
-
|
136 |
-
|
|
|
|
|
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 |
-
|
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 |
-
|
177 |
-
|
178 |
-
const
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
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}
|