UNPKG

flowviz

Version:

A framework which provides seamless integration with other phylogenetic tools and frameworks, while allowing workflow scheduling and execution, through the Apache Airflow workflow system.

358 lines (311 loc) 9.6 kB
import SendIcon from "@mui/icons-material/Send"; import { Box, Button, Grid } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import React, { useCallback, useRef, useState, useEffect } from "react"; import ReactFlow, { addEdge, Background, Controls, MiniMap, ReactFlowProvider, updateEdge, useEdgesState, useNodesState, } from "react-flow-renderer"; import { useNavigate } from "react-router-dom"; import GenericErrorBar from "../component/common/genericErrorBar"; import Loading from "../component/common/loading"; import ToolNode from "../component/whiteboard/task/toolNode"; import WorkflowSubmitDialog from "../component/whiteboard/workflowSubmitDialog"; let id = -1; const getId = () => `node${++id}`; const nodeTypes = { tool: ToolNode }; const edgeOptions = { animated: true, style: { stroke: "black", }, markerEnd: { type: "arrowclosed", color: "black", }, }; export default function Whiteboard({ toolService, workflowService, setDrawerList, }) { // Get NavBar height from theme const theme = useTheme(); const navigate = useNavigate(); const appBarHeight = theme.mixins.toolbar.minHeight; const reactFlowWrapper = useRef(null); // Whiteboard GUI state const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [reactFlowInstance, setReactFlowInstance] = useState(null); const [nodeChanged, setNodeChanged] = useState(null); // States if the workflow can be commited const [isSubmitting, setIsSubmitting] = useState(false); // Is the user dropping a tool into the whiteboard const [isToolDrop, setIsToolDrop] = useState(false); const [droppingToolName, setDroppingToolName] = useState(""); const [droppingToolPos, setDroppingToolPos] = useState(null); // State for fetched tools to avoid unnecessarry HTTP requests const [fetchedTools, setFetchedTools] = useState(new Map()); // Workflow name and datetimes for workflow submission const [dialogOpen, setDialogOpen] = useState(false); const [workflowSubmission, setWorkflowSubmission] = useState({ workflowName: "", workflowDescription: "", startDateTime: new Date(), }); // Updates relayed data if a node changed useEffect(() => { const eds = [...edges]; const targets = []; eds.forEach((e) => { if (e.source === nodeChanged) { targets.push(e.target); } }); targets.forEach((t) => updateNodeInputs(nodeChanged, t)); }, [nodeChanged]); const onWorkflowSubmission = ( workflowName, workflowDescription, startDateTime ) => { setWorkflowSubmission({ workflowName: workflowName, workflowDescription: workflowDescription, startDateTime: startDateTime, }); setIsSubmitting(true); }; toolService.getTools(GenericErrorBar, setDrawerList, <Loading />); const onNodeSetupUpdate = useCallback((nodeId, data) => { setNodes((nds) => { return nds.map((node) => { if (node.id === nodeId) { node.data = data; setNodeChanged(node.id); } return node; }); }); }); // Detects and avoids loops inside the drawn workflow const hasLoop = useCallback((currEdges, currTarget) => { if (currEdges.length <= 0) return false; const targetEdgeNode = currEdges.find((e) => e.source === currTarget); if (!targetEdgeNode) return false; return targetEdgeNode.target !== null; }); const updateNodeInputs = useCallback((sourceNodeId, targetNodeId) => { setNodes((nds) => { const sourceNode = nds.find((n) => n.id === sourceNodeId); return nds.map((n) => { if (n.id === targetNodeId) { n.data.config.inputs.push(sourceNode.data.config.outputs); } return n; }); }); }); // Set edges const onConnect = useCallback((params) => { const source = params.source; const target = params.target; setEdges((eds) => { if (hasLoop(eds, target)) return eds; return addEdge(params, eds); }); updateNodeInputs(source, target); }, []); // Update edges const onEdgeUpdate = useCallback((oldEdge, newConnection) => { const source = oldEdge.source; const target = newConnection.target; setEdges((els) => { if (hasLoop(els, target)) return els; return updateEdge(oldEdge, newConnection, els); }); updateNodeInputs(source, target); }); const onDragOver = useCallback((event) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; }, []); const OnToolDrop = () => { const onSuccess = (tool) => { setFetchedTools((fetchedToolsMap) => fetchedToolsMap.set(droppingToolName, tool) ); const node = { id: getId(), type: "tool", position: droppingToolPos, data: { tool: tool, name: "", config: { inputs: [], outputs: [], setup: {}, }, onNodeUpdate: onNodeSetupUpdate, }, }; setNodes((nds) => nds.concat(node)); setDroppingToolName(""); setDroppingToolPos(null); setIsToolDrop(false); }; // Checks if tool was already fetched const tool = fetchedTools.get(droppingToolName); if (!tool) { // Fetches tool if not found inside state toolService.getTool( droppingToolName, GenericErrorBar, onSuccess, <Loading /> ); } else { // Creates node if tool was found inside state onSuccess(tool); } return <></>; }; const onDrop = useCallback( async (event) => { event.preventDefault(); const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); const toolName = event.dataTransfer.getData("application/reactflow"); const position = reactFlowInstance.project({ x: event.clientX - reactFlowBounds.left, y: event.clientY - reactFlowBounds.top, }); setDroppingToolName(toolName); setDroppingToolPos(position); setIsToolDrop(true); }, [reactFlowInstance] ); function WorkflowRequest() { const workflowRequest = getWorkflowRequest( workflowSubmission, nodes, edges ); const OnSuccess = () => { navigate("/submission", { state: { text: `Workflow ${workflowSubmission.workflowName} was successfully submitted!`, resourcePageLabel: workflowSubmission.workflowName, resourcePageUrl: `/workflow/${workflowSubmission.workflowName}`, }, }); return <></>; }; return workflowService.postWorkflow( JSON.stringify(workflowRequest), (error) => <GenericErrorBar error={error} />, OnSuccess, <Loading /> ); } return ( <Grid container> <Grid item> <ReactFlowProvider> <div ref={reactFlowWrapper} style={{ height: `calc(100vh - ${appBarHeight * 2}px)`, width: "100vw", }} > <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} defaultEdgeOptions={edgeOptions} onConnect={onConnect} onInit={setReactFlowInstance} onDragOver={onDragOver} onDrop={onDrop} onEdgeUpdate={onEdgeUpdate} deleteKeyCode={"Delete"} nodeTypes={nodeTypes} > <MiniMap /> <Controls /> <Background variant="lines" color="#bbb" gap={20} /> </ReactFlow> </div> </ReactFlowProvider> </Grid> <Grid item> <Box width="100vw" display="flex" justifyContent="flex-end" alignItems="flex-end" > <Button variant="outlined" endIcon={<SendIcon />} onClick={() => setDialogOpen(true)} sx={{ mr: 2 }} > Submit </Button> </Box> <WorkflowSubmitDialog open={dialogOpen} onApply={(workflowName, description, startDateTime) => { onWorkflowSubmission(workflowName, description, startDateTime); setDialogOpen(false); }} onCancel={() => setDialogOpen(false)} /> {isToolDrop ? <OnToolDrop /> : <></>} {isSubmitting ? <WorkflowRequest /> : <></>} </Grid> </Grid> ); } function getWorkflowRequest(workflowSubmission, nodes, edges) { const workflow = []; const name = workflowSubmission.workflowName; const description = workflowSubmission.workflowDescription; const startDateTime = workflowSubmission.startDateTime; nodes.forEach((node) => { const nodeId = node.id; const data = node.data; const toolName = data.tool.general.name; const type = data.tool.access._type; const action = data.config.action; const children = edges.map((edge) => { if (edge.source.includes(nodeId)) { return nodes.find((node) => node.id === edge.target).data.name; } }); const step = { id: data.name, tool: toolName, action: type === "library" ? { command: action } : {}, children: children, }; workflow.push(step); }); return { name: name, description: description, startDateTime: startDateTime, tasks: workflow, }; }