// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) import { Background, Controls, Panel, ReactFlow, ReactFlowProvider, MarkerType, getOutgoers } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import warningStyles from './visualProgrammingUI/components/WarningSidebar.module.css' import {type CSSProperties, useEffect, useState} from "react"; import {useShallow} from 'zustand/react/shallow'; import useProgramStore from "../../utils/programStore.ts"; import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx'; import {type EditorWarning, globalWarning} from "./visualProgrammingUI/components/EditorWarnings.tsx"; import {WarningsSidebar} from "./visualProgrammingUI/components/WarningSidebar.tsx"; import useFlowStore from './visualProgrammingUI/VisProgStores.tsx'; import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx'; import styles from './VisProg.module.css' import {NodeTypes} from './visualProgrammingUI/NodeRegistry.ts'; import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx'; import MonitoringPage from '../MonitoringPage/MonitoringPage.tsx'; import {graphReducer, runProgram} from './VisProgLogic.ts'; // --| config starting params for flow |-- /** * defines how the default edge looks inside the editor */ const DEFAULT_EDGE_OPTIONS = { type: 'default', markerEnd: { type: MarkerType.ArrowClosed, color: '#505050', }, }; /** * defines what functions in the FlowState store map to which names, * @param state */ const selector = (state: FlowState) => ({ nodes: state.nodes, edges: state.edges, onNodesChange: state.onNodesChange, onNodesDelete: state.onNodesDelete, onEdgesDelete: state.onEdgesDelete, onEdgesChange: state.onEdgesChange, onConnect: state.onConnect, onReconnectStart: state.onReconnectStart, onReconnectEnd: state.onReconnectEnd, onReconnect: state.onReconnect, undo: state.undo, redo: state.redo, beginBatchAction: state.beginBatchAction, endBatchAction: state.endBatchAction, scrollable: state.scrollable }); // --| 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 { nodes, edges, onNodesChange, onNodesDelete, onEdgesDelete, onEdgesChange, onConnect, onReconnect, onReconnectStart, onReconnectEnd, undo, redo, beginBatchAction, endBatchAction, scrollable } = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore const [zoom, setZoom] = useState(1); // adds ctrl+z and ctrl+y support to respectively undo and redo actions useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === 'z') undo(); if (e.ctrlKey && e.key === 'y') redo(); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }); const {unregisterWarning, registerWarning} = useFlowStore(); useEffect(() => { if (checkPhaseChain()) { unregisterWarning(globalWarning,'INCOMPLETE_PROGRAM'); } else { // create global warning for incomplete program chain const incompleteProgramWarning : EditorWarning = { scope: { id: globalWarning, handleId: undefined }, type: 'INCOMPLETE_PROGRAM', severity: "ERROR", description: "there is no complete phase chain from the startNode to the EndNode" } registerWarning(incompleteProgramWarning); } },[edges, registerWarning, unregisterWarning]) return (
setZoom(viewport.zoom)} reconnectRadius={15} snapToGrid fitView proOptions={{hideAttribution: true}} style={{flexGrow: 3}} > {/* contains the drag and drop panel for nodes */}
); }; /** * Places the VisProgUI component inside a ReactFlowProvider * * Wrapping the editor component inside a ReactFlowProvider * allows us to access and interact with the components inside the editor, outside the editor definition, * thus facilitating the addition of node specific functions inside their node definitions */ function VisualProgrammingUI() { return ( ); } const checkPhaseChain = (): boolean => { const {nodes, edges} = useFlowStore.getState(); function checkForCompleteChain(currentNodeId: string): boolean { const outgoingPhases = getOutgoers({id: currentNodeId}, nodes, edges) .filter(node => ["end", "phase"].includes(node.type!)); if (outgoingPhases.length === 0) return false; if (outgoingPhases.some(node => node.type === "end" )) return true; const next = outgoingPhases.map(node => checkForCompleteChain(node.id)) .find(result => result); return !!next; } return checkForCompleteChain('start'); }; /** * houses the entire page, so also UI elements * that are not a part of the Visual Programming UI * @constructor */ function VisProgPage() { const [showSimpleProgram, setShowSimpleProgram] = useState(false); const [programValidity, setProgramValidity] = useState(true); const {isProgramValid, severityIndex} = useFlowStore(); const setProgramState = useProgramStore((state) => state.setProgramState); const validity = () => {return isProgramValid();} useEffect(() => { setProgramValidity(validity); // the following eslint disable is required as it wants us to use all possible dependencies for the useEffect statement, // however this would cause unneeded updates // eslint-disable-next-line react-hooks/exhaustive-deps }, [severityIndex]); const processProgram = () => { const phases = graphReducer(); // reduce graph setProgramState({ phases }); // <-- save to store setShowSimpleProgram(true); // show SimpleProgram runProgram(); // send to backend if needed }; if (showSimpleProgram) { return (
); } return ( <> ) } export default VisProgPage