Spaces:
Running
Running
// The LynxKite workspace editor. | |
import { useParams } from "react-router"; | |
import useSWR from 'swr'; | |
import { useMemo, useCallback, useState } from "react"; | |
import favicon from '../assets/favicon.ico'; | |
import { | |
ReactFlow, | |
Controls, | |
MarkerType, | |
useReactFlow, | |
ReactFlowProvider, | |
applyEdgeChanges, | |
applyNodeChanges, | |
type XYPosition, | |
type Node, | |
type Edge, | |
type Connection, | |
type NodeTypes, | |
} from '@xyflow/react'; | |
// @ts-ignore | |
import ArrowBack from '~icons/tabler/arrow-back.jsx'; | |
// @ts-ignore | |
import Backspace from '~icons/tabler/backspace.jsx'; | |
// @ts-ignore | |
import Atom from '~icons/tabler/atom.jsx'; | |
import { syncedStore, getYjsDoc } from "@syncedstore/core"; | |
import { useSyncedStore } from "@syncedstore/react"; | |
import { WebsocketProvider } from "y-websocket"; | |
import NodeWithParams from './nodes/NodeWithParams'; | |
// import NodeWithVisualization from './NodeWithVisualization'; | |
// import NodeWithImage from './NodeWithImage'; | |
// import NodeWithTableView from './NodeWithTableView'; | |
// import NodeWithSubFlow from './NodeWithSubFlow'; | |
// import NodeWithArea from './NodeWithArea'; | |
// import NodeSearch from './NodeSearch'; | |
import EnvironmentSelector from './EnvironmentSelector'; | |
import { LynxKiteState } from './LynxKiteState'; | |
import '@xyflow/react/dist/style.css'; | |
import { Workspace } from "../apiTypes.ts"; | |
export default function (props: any) { | |
return ( | |
<ReactFlowProvider> | |
<LynxKiteFlow {...props} /> | |
</ReactFlowProvider> | |
); | |
} | |
function LynxKiteFlow() { | |
const reactFlow = useReactFlow(); | |
const [nodes, setNodes] = useState([] as Node[]); | |
const [edges, setEdges] = useState([] as Edge[]); | |
const { path } = useParams(); | |
const sstore = syncedStore({ workspace: {} as Workspace }); | |
const doc = getYjsDoc(sstore); | |
const wsProvider = useMemo(() => new WebsocketProvider("ws://localhost:8000/ws/crdt", path!, doc), [path]); | |
wsProvider; // Just to disable the lint warning. The life cycle of this object is a mystery. | |
const state = useSyncedStore(sstore); | |
const onNodesChange = useCallback( | |
(changes: any[]) => { | |
setNodes((nds) => applyNodeChanges(changes, nds)); | |
const wnodes = state.workspace!.nodes!; | |
for (const ch of changes) { | |
const nodeIndex = wnodes.findIndex((n) => n.id === ch.id); | |
if (nodeIndex === -1) continue; | |
const node = wnodes[nodeIndex]; | |
if (!node) continue; | |
if (ch.type === 'position') { | |
node.position = ch.position; | |
} else if (ch.type === 'select') { | |
} else if (ch.type === 'dimensions') { | |
} else if (ch.type === 'replace') { | |
node.data.collapsed = ch.item.data.collapsed; | |
node.data.params = { ...ch.item.data.params }; | |
} else { | |
console.log('Unknown node change', ch); | |
} | |
} | |
}, | |
[], | |
); | |
const onEdgesChange = useCallback( | |
(changes: any[]) => setEdges((eds) => applyEdgeChanges(changes, eds)), | |
[], | |
); | |
if (state?.workspace?.nodes && JSON.stringify(nodes) !== JSON.stringify([...state.workspace.nodes as Node[]])) { | |
const updated = Object.fromEntries(state.workspace.nodes.map((n) => [n.id, n])); | |
const oldNodes = Object.fromEntries(nodes.map((n) => [n.id, n])); | |
const updatedNodes = nodes.filter(n => updated[n.id]).map((n) => ({ ...n, ...updated[n.id] })) as Node[]; | |
const newNodes = state.workspace.nodes.filter((n) => !oldNodes[n.id]); | |
const allNodes = [...updatedNodes, ...newNodes]; | |
if (JSON.stringify(allNodes) !== JSON.stringify(nodes)) { | |
setNodes(allNodes as Node[]); | |
} | |
} | |
if (state?.workspace?.edges && JSON.stringify(edges) !== JSON.stringify([...state.workspace.edges as Edge[]])) { | |
const updated = Object.fromEntries(state.workspace.edges.map((e) => [e.id, e])); | |
const oldEdges = Object.fromEntries(edges.map((e) => [e.id, e])); | |
const updatedEdges = edges.filter(e => updated[e.id]).map((e) => ({ ...e, ...updated[e.id] })) as Edge[]; | |
const newEdges = state.workspace.edges.filter((e) => !oldEdges[e.id]); | |
const allEdges = [...updatedEdges, ...newEdges]; | |
if (JSON.stringify(allEdges) !== JSON.stringify(edges)) { | |
setEdges(allEdges as Edge[]); | |
} | |
} | |
const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json()); | |
const catalog = useSWR('/api/catalog', fetcher); | |
const nodeTypes = useMemo(() => ({ | |
basic: NodeWithParams, | |
table_view: NodeWithParams, | |
}), []); | |
const parentDir = path!.split('/').slice(0, -1).join('/'); | |
return ( | |
<div className="workspace"> | |
<div className="top-bar bg-neutral"> | |
<a className="logo" href=""><img src={favicon} /></a> | |
<div className="ws-name"> | |
{path} | |
</div> | |
<EnvironmentSelector | |
options={Object.keys(catalog.data || {})} | |
value={state.workspace.env!} | |
onChange={(env) => { state.workspace.env = env; }} | |
/> | |
<div className="tools text-secondary"> | |
<a href=""><Atom /></a> | |
<a href=""><Backspace /></a> | |
<a href={'/dir/' + parentDir}><ArrowBack /></a> | |
</div> | |
</div> | |
<div style={{ height: "100%", width: '100vw' }}> | |
<LynxKiteState.Provider value={state}> | |
<ReactFlow | |
nodes={nodes} | |
edges={edges} | |
nodeTypes={nodeTypes} fitView | |
onNodesChange={onNodesChange} | |
onEdgesChange={onEdgesChange} | |
proOptions={{ hideAttribution: true }} | |
maxZoom={3} | |
minZoom={0.3} | |
defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }} | |
> | |
<Controls /> | |
{/* {#if nodeSearchSettings} | |
<NodeSearch pos={nodeSearchSettings.pos} boxes={nodeSearchSettings.boxes} on:cancel={closeNodeSearch} on:add={addNode} /> | |
{/if} */} | |
</ReactFlow> | |
</LynxKiteState.Provider> | |
</div> | |
</div> | |
); | |
} | |