Spaces:
Running
Running
Use straight Y.js to observe backend updates.
Browse files- web/src/workspace/Workspace.tsx +54 -54
web/src/workspace/Workspace.tsx
CHANGED
@@ -1,16 +1,16 @@
|
|
1 |
// The LynxKite workspace editor.
|
2 |
import { useParams } from "react-router";
|
3 |
import useSWR from 'swr';
|
4 |
-
import { useMemo, useCallback, useState } from "react";
|
5 |
import favicon from '../assets/favicon.ico';
|
6 |
import {
|
7 |
ReactFlow,
|
8 |
Controls,
|
9 |
MarkerType,
|
10 |
-
useReactFlow,
|
11 |
ReactFlowProvider,
|
12 |
applyEdgeChanges,
|
13 |
applyNodeChanges,
|
|
|
14 |
type XYPosition,
|
15 |
type Node,
|
16 |
type Edge,
|
@@ -24,7 +24,6 @@ import Backspace from '~icons/tabler/backspace.jsx';
|
|
24 |
// @ts-ignore
|
25 |
import Atom from '~icons/tabler/atom.jsx';
|
26 |
import { syncedStore, getYjsDoc } from "@syncedstore/core";
|
27 |
-
import { useSyncedStore } from "@syncedstore/react";
|
28 |
import { WebsocketProvider } from "y-websocket";
|
29 |
import NodeWithParams from './nodes/NodeWithParams';
|
30 |
// import NodeWithVisualization from './NodeWithVisualization';
|
@@ -48,64 +47,65 @@ export default function (props: any) {
|
|
48 |
|
49 |
|
50 |
function LynxKiteFlow() {
|
51 |
-
const
|
52 |
const [nodes, setNodes] = useState([] as Node[]);
|
53 |
const [edges, setEdges] = useState([] as Edge[]);
|
54 |
const { path } = useParams();
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
if (
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
} else if (ch.type === 'dimensions') {
|
74 |
-
} else if (ch.type === 'replace') {
|
75 |
-
node.data.collapsed = ch.item.data.collapsed;
|
76 |
-
node.data.params = { ...ch.item.data.params };
|
77 |
-
} else {
|
78 |
-
console.log('Unknown node change', ch);
|
79 |
}
|
80 |
}
|
81 |
-
}
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
[],
|
87 |
-
);
|
88 |
-
|
89 |
-
if (state?.workspace?.nodes && JSON.stringify(nodes) !== JSON.stringify([...state.workspace.nodes as Node[]])) {
|
90 |
-
const updated = Object.fromEntries(state.workspace.nodes.map((n) => [n.id, n]));
|
91 |
-
const oldNodes = Object.fromEntries(nodes.map((n) => [n.id, n]));
|
92 |
-
const updatedNodes = nodes.filter(n => updated[n.id]).map((n) => ({ ...n, ...updated[n.id] })) as Node[];
|
93 |
-
const newNodes = state.workspace.nodes.filter((n) => !oldNodes[n.id]);
|
94 |
-
const allNodes = [...updatedNodes, ...newNodes];
|
95 |
-
if (JSON.stringify(allNodes) !== JSON.stringify(nodes)) {
|
96 |
-
setNodes(allNodes as Node[]);
|
97 |
}
|
98 |
-
}
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
const
|
105 |
-
if (
|
106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
}
|
108 |
-
}
|
|
|
|
|
|
|
109 |
|
110 |
const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
|
111 |
const catalog = useSWR('/api/catalog', fetcher);
|
|
|
1 |
// The LynxKite workspace editor.
|
2 |
import { useParams } from "react-router";
|
3 |
import useSWR from 'swr';
|
4 |
+
import { useEffect, useMemo, useCallback, useState } from "react";
|
5 |
import favicon from '../assets/favicon.ico';
|
6 |
import {
|
7 |
ReactFlow,
|
8 |
Controls,
|
9 |
MarkerType,
|
|
|
10 |
ReactFlowProvider,
|
11 |
applyEdgeChanges,
|
12 |
applyNodeChanges,
|
13 |
+
useUpdateNodeInternals,
|
14 |
type XYPosition,
|
15 |
type Node,
|
16 |
type Edge,
|
|
|
24 |
// @ts-ignore
|
25 |
import Atom from '~icons/tabler/atom.jsx';
|
26 |
import { syncedStore, getYjsDoc } from "@syncedstore/core";
|
|
|
27 |
import { WebsocketProvider } from "y-websocket";
|
28 |
import NodeWithParams from './nodes/NodeWithParams';
|
29 |
// import NodeWithVisualization from './NodeWithVisualization';
|
|
|
47 |
|
48 |
|
49 |
function LynxKiteFlow() {
|
50 |
+
const updateNodeInternals = useUpdateNodeInternals()
|
51 |
const [nodes, setNodes] = useState([] as Node[]);
|
52 |
const [edges, setEdges] = useState([] as Edge[]);
|
53 |
const { path } = useParams();
|
54 |
+
const [state, setState] = useState({ workspace: {} as Workspace });
|
55 |
+
useEffect(() => {
|
56 |
+
const state = syncedStore({ workspace: {} as Workspace });
|
57 |
+
setState(state);
|
58 |
+
const doc = getYjsDoc(state);
|
59 |
+
const wsProvider = new WebsocketProvider("ws://localhost:8000/ws/crdt", path!, doc);
|
60 |
+
const onChange = (update: any, origin: any, doc: any, tr: any) => {
|
61 |
+
if (origin === wsProvider) {
|
62 |
+
// An update from the CRDT. Apply it to the local state.
|
63 |
+
// This is only necessary because ReactFlow keeps secret internal copies of our stuff.
|
64 |
+
if (!state.workspace) return;
|
65 |
+
if (!state.workspace.nodes) return;
|
66 |
+
if (!state.workspace.edges) return;
|
67 |
+
setNodes([...state.workspace.nodes] as Node[]);
|
68 |
+
setEdges([...state.workspace.edges] as Edge[]);
|
69 |
+
for (const node of state.workspace.nodes) {
|
70 |
+
// Make sure the internal copies are updated.
|
71 |
+
updateNodeInternals(node.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
}
|
73 |
}
|
74 |
+
};
|
75 |
+
doc.on('update', onChange);
|
76 |
+
return () => {
|
77 |
+
doc.destroy();
|
78 |
+
wsProvider.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
}
|
80 |
+
}, [path]);
|
81 |
+
|
82 |
+
const onNodesChange = (changes: any[]) => {
|
83 |
+
// An update from the UI. Apply it to the local state...
|
84 |
+
setNodes((nds) => applyNodeChanges(changes, nds));
|
85 |
+
// ...and to the CRDT state. (Which could be the same, except for ReactFlow's internal copies.)
|
86 |
+
const wnodes = state.workspace?.nodes;
|
87 |
+
if (!wnodes) return;
|
88 |
+
for (const ch of changes) {
|
89 |
+
const nodeIndex = wnodes.findIndex((n) => n.id === ch.id);
|
90 |
+
if (nodeIndex === -1) continue;
|
91 |
+
const node = wnodes[nodeIndex];
|
92 |
+
if (!node) continue;
|
93 |
+
// Position events sometimes come with NaN values. Ignore them.
|
94 |
+
if (ch.type === 'position' && !isNaN(ch.position.x) && !isNaN(ch.position.y)) {
|
95 |
+
Object.assign(node.position, ch.position);
|
96 |
+
} else if (ch.type === 'select') {
|
97 |
+
} else if (ch.type === 'dimensions') {
|
98 |
+
} else if (ch.type === 'replace') {
|
99 |
+
node.data.collapsed = ch.item.data.collapsed;
|
100 |
+
node.data.params = { ...ch.item.data.params };
|
101 |
+
} else {
|
102 |
+
console.log('Unknown node change', ch);
|
103 |
+
}
|
104 |
}
|
105 |
+
};
|
106 |
+
const onEdgesChange = (changes: any[]) => {
|
107 |
+
setEdges((eds) => applyEdgeChanges(changes, eds));
|
108 |
+
};
|
109 |
|
110 |
const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
|
111 |
const catalog = useSWR('/api/catalog', fetcher);
|