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}
|