feat: added ReactFlow-based node graph #11

Merged
j.gerla merged 68 commits from feat/visual-programming-interface into dev 2025-10-28 11:59:03 +00:00
6 changed files with 75 additions and 24 deletions
Showing only changes of commit a9effb7c23 - Show all commits

View File

@@ -1,4 +1,4 @@
import { Link } from 'react-router' import {Link} from "react-router";
import VisProgUI from "./visualProgrammingUI/VisProgUI.tsx"; import VisProgUI from "./visualProgrammingUI/VisProgUI.tsx";
@@ -9,8 +9,8 @@ function VisProgPage() {
return ( return (
<> <>
<VisProgUI/> <VisProgUI/>
<Link to = {"/"}> {/* here you link to the homepage, in App.tsx you can link new pages */} <Link to={'/'}> {/* here you link to the homepage, in App.tsx you can link new pages */}
<button className= 'movePage left' onClick={() :void => {}}> <button className="movePage left" onClick={(): void => {}}>
Page {'<'}-- Go Home Page {'<'}-- Go Home
</button> </button>
</Link> </Link>

View File

@@ -8,6 +8,10 @@ import {
import {type FlowState} from './VisProgTypes.tsx'; import {type FlowState} from './VisProgTypes.tsx';
/**
* contains the nodes that are created when the editor is loaded,
* should contain at least a start and an end node
*/
const initialNodes = [ const initialNodes = [
{ {
id: 'start', id: 'start',
@@ -29,6 +33,9 @@ const initialNodes = [
} }
]; ];
/**
* contains the initial edges that are created when the editor is loaded
*/
const initialEdges = [ const initialEdges = [
{ {
id: 'start-end', id: 'start-end',
@@ -37,7 +44,11 @@ const initialEdges = [
} }
]; ];
// this is our useStore hook that we can use in our components to get parts of the store and call actions /**
* The useFlowStore hook contains the implementation for editor functionality and state
* we can use this inside our editor component to access the current state
* and use any implemented functionality
*/
const useFlowStore = create<FlowState>((set, get) => ({ const useFlowStore = create<FlowState>((set, get) => ({
nodes: initialNodes, nodes: initialNodes,
edges: initialEdges, edges: initialEdges,

View File

@@ -7,11 +7,19 @@ import {
type OnReconnect, type OnReconnect,
} from '@xyflow/react'; } from '@xyflow/react';
/**
* a type meant to house different node types, currently not used
* but will allow us to more clearly define nodeTypes when we implement
* computation of the Graph inside the ReactFlow editor
*/
export type AppNode = Node; export type AppNode = Node;
/**
* The type for the Zustand store object used to manage the state of the ReactFlow editor
*/
export type FlowState = { export type FlowState = {
nodes: Node[]; nodes: AppNode[];
edges: Edge[]; edges: Edge[];
edgeReconnectSuccessful: boolean; edgeReconnectSuccessful: boolean;
onNodesChange: OnNodesChange; onNodesChange: OnNodesChange;

View File

@@ -1,5 +1,3 @@
import './VisProgUI.css'
import { import {
Background, Background,
Controls, Controls,
@@ -9,18 +7,18 @@ import {
MarkerType, MarkerType,
} from '@xyflow/react'; } from '@xyflow/react';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import {useShallow} from 'zustand/react/shallow';
import { import {
StartNode, StartNode,
EndNode, EndNode,
PhaseNode, PhaseNode,
NormNode NormNode
} from './components/NodeDefinitions.tsx'; } from './components/NodeDefinitions.tsx';
import {DndToolbar} from './components/DragDropSidebar.tsx';
import {Sidebar} from './components/DragDropSidebar.tsx';
import useFlowStore from './VisProgStores.tsx'; import useFlowStore from './VisProgStores.tsx';
import {useShallow} from 'zustand/react/shallow';
import type {FlowState} from './VisProgTypes.tsx'; import type {FlowState} from './VisProgTypes.tsx';
import './VisProgUI.css'
// --| config starting params for flow |-- // --| config starting params for flow |--
@@ -45,6 +43,11 @@ const DEFAULT_EDGE_OPTIONS = {
}, },
}; };
/**
* defines what functions in the FlowState store map to which names,
* @param state
*/
const selector = (state: FlowState) => ({ const selector = (state: FlowState) => ({
nodes: state.nodes, nodes: state.nodes,
edges: state.edges, edges: state.edges,
@@ -58,6 +61,12 @@ const selector = (state: FlowState) => ({
// --| define ReactFlow editor |-- // --| define ReactFlow editor |--
/**
* Defines the ReactFlow visual programming editor component
* any implementations of editor logic should be encapsulated where possible
* so the Component definition stays as readable as possible
* @constructor
*/
const VisProgUI = () => { const VisProgUI = () => {
const { const {
nodes, edges, nodes, edges,
@@ -70,8 +79,8 @@ const VisProgUI = () => {
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore } = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
return ( return (
<div className={'outer-editor-container'}> <div className={"outer-editor-container"}>
<div className={'inner-editor-container'}> <div className={"inner-editor-container"}>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
@@ -87,8 +96,8 @@ const VisProgUI = () => {
fitView fitView
proOptions={{hideAttribution: true}} proOptions={{hideAttribution: true}}
> >
<Panel position="top-center" className={'dnd-panel'}> <Panel position="top-center" className={"dnd-panel"}>
<Sidebar/> {/* contains the drag and drop panel for nodes */} <DndToolbar/> {/* contains the drag and drop panel for nodes */}
</Panel> </Panel>
<Controls/> <Controls/>
<Background/> <Background/>

View File

@@ -10,11 +10,14 @@ import {
useState useState
} from 'react'; } from 'react';
// Is used to make sure each subsequent node gets a unique id, so the ReactFlow implementation
// can distinguish between different nodes.
let id = 0; let id = 0;
const getId = () => `dndnode_${id++}`; const getId = () => `dndnode_${id++}`;
/**
* DraggableNodeProps dictates the type properties of a DraggableNode
*/
interface DraggableNodeProps { interface DraggableNodeProps {
className?: string; className?: string;
children: ReactNode; children: ReactNode;
@@ -22,11 +25,21 @@ interface DraggableNodeProps {
onDrop: (nodeType: string, position: XYPosition) => void; onDrop: (nodeType: string, position: XYPosition) => void;
} }
/**
* Definition of a node inside the drag and drop toolbar,
* these nodes require an onDrop function to be supplied
* that dictates how the node is created in the graph.
*
* @param className
* @param children
* @param nodeType
* @param onDrop
* @constructor
*/
function DraggableNode({className, children, nodeType, onDrop}: DraggableNodeProps) { function DraggableNode({className, children, nodeType, onDrop}: DraggableNodeProps) {
const draggableRef = useRef<HTMLDivElement>(null); const draggableRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<XYPosition>({x: 0, y: 0}); const [position, setPosition] = useState<XYPosition>({x: 0, y: 0});
// @ts-ignore // @ts-ignore
useDraggable(draggableRef, { useDraggable(draggableRef, {
position: position, position: position,
@@ -54,9 +67,17 @@ function DraggableNode({className, children, nodeType, onDrop}: DraggableNodePro
); );
} }
export function Sidebar() { /**
* the DndToolbar defines how the drag and drop toolbar component works
* and includes the default onDrop behavior through handleNodeDrop
* @constructor
*/
export function DndToolbar() {
const {setNodes, screenToFlowPosition} = useReactFlow(); const {setNodes, screenToFlowPosition} = useReactFlow();
/**
* handleNodeDrop implements the default onDrop behavior
*/
const handleNodeDrop = useCallback( const handleNodeDrop = useCallback(
(nodeType: string, screenPosition: XYPosition) => { (nodeType: string, screenPosition: XYPosition) => {
const flow = document.querySelector('.react-flow'); const flow = document.querySelector('.react-flow');
@@ -120,7 +141,7 @@ export function Sidebar() {
); );
return ( return (
<aside style={{ <div style={{
padding: '10px 10px', padding: '10px 10px',
outline: '2.5pt solid black', outline: '2.5pt solid black',
borderRadius: '0pt 0pt 5pt 5pt', borderRadius: '0pt 0pt 5pt 5pt',
@@ -147,6 +168,6 @@ export function Sidebar() {
norm Node norm Node
</DraggableNode> </DraggableNode>
</div> </div>
</aside> </div>
); );
} }

View File

@@ -2,7 +2,9 @@ import {Handle, NodeToolbar, Position, useReactFlow} from '@xyflow/react';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import '../VisProgUI.css'; import '../VisProgUI.css';
// Datatypes for NodeTypes // Contains the datatypes for the data inside our NodeTypes
// this has to be improved or adapted to suit our implementation for computing the graph
// into a format that is useful for the Control Backend
type defaultNodeData = { type defaultNodeData = {
label: string; label: string;