diff --git a/src/api/ToolboxAPI.ts b/src/api/ToolboxAPI.ts index 53b3561..698d525 100644 --- a/src/api/ToolboxAPI.ts +++ b/src/api/ToolboxAPI.ts @@ -1,4 +1,5 @@ import { MetaSolverSetting } from "./data-model/MetaSolverSettings"; +import { ProblemGraph } from "./data-model/ProblemGraph"; import { ProblemSolver } from "./data-model/ProblemSolver"; import { Solution } from "./data-model/Solution"; import { SolutionStatus } from "./data-model/SolutionStatus"; @@ -40,6 +41,17 @@ export async function postProblem( export async function fetchSolvers( problemType: string ): Promise { + // Return mock promise + // todo remove + return new Promise((resolve) => { + setTimeout(() => { + resolve([ + { id: "tsp1", name: "TSP 1" }, + { id: "tsp2", name: "TSP 2" }, + ]); + }, 1000); + }); + return fetch(`${baseUrl()}/solvers/${problemType}`, { method: "GET", headers: { @@ -93,3 +105,37 @@ export async function fetchMetaSolverSettings( return []; }); } + +export async function fetchProblemGraph(): Promise { + return new Promise((resolve) => { + return resolve({ + start: { + problemType: "vehicle-routing", + status: SolutionStatus.SOLVED, + solver: { + id: "vr-solver-1", + name: "VR Solver 1" + }, + solutionId: 1, + subRoutines: [ + { + problemType: "clustering", + status: SolutionStatus.COMPUTING, + solutionId: 2, + solver: { + id: "clustering-solver-1", + name: "Clustering Solver 1" + }, + subRoutines: [] + }, + { + problemType: "travelling-salesman", + status: SolutionStatus.PENDING_USER_ACTION, + solutionId: 3, + subRoutines: [] + } + ] + } + }); + }); +} diff --git a/src/api/data-model/ProblemGraph.ts b/src/api/data-model/ProblemGraph.ts new file mode 100644 index 0000000..02bc846 --- /dev/null +++ b/src/api/data-model/ProblemGraph.ts @@ -0,0 +1,14 @@ +import { ProblemSolver } from "./ProblemSolver"; +import { SolutionStatus } from "./SolutionStatus"; + +export interface ProblemGraph { + start: ProblemNode; +} + +export interface ProblemNode { + problemType: string; + status: SolutionStatus; + solutionId: number; + solver?: ProblemSolver; + subRoutines: ProblemNode[]; +} diff --git a/src/api/data-model/SolutionStatus.ts b/src/api/data-model/SolutionStatus.ts index 8f65e4f..5c0bf3c 100644 --- a/src/api/data-model/SolutionStatus.ts +++ b/src/api/data-model/SolutionStatus.ts @@ -2,4 +2,5 @@ export enum SolutionStatus { INVALID, COMPUTING, SOLVED, + PENDING_USER_ACTION, } diff --git a/src/components/solvers/Graph/ProblemGraphView.tsx b/src/components/solvers/Graph/ProblemGraphView.tsx new file mode 100644 index 0000000..41f0db0 --- /dev/null +++ b/src/components/solvers/Graph/ProblemGraphView.tsx @@ -0,0 +1,159 @@ +import { useEffect } from "react"; +import { Controls, Edge, Node, ReactFlow, useEdgesState, useNodesState } from "reactflow"; +import { ProblemGraph, ProblemNode } from "../../../api/data-model/ProblemGraph"; +import { SolutionStatus } from "../../../api/data-model/SolutionStatus"; +import { fetchSolvers } from "../../../api/ToolboxAPI"; +import 'reactflow/dist/style.css'; + +export const ProblemGraphView = (props: { graph: ProblemGraph | null }) => { + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + useEffect(() => { + if (!props.graph) return; + + let { nodes, edges } = problemGraphToReactFlow(props.graph); + setEdges(edges); + setNodes(nodes); + + function problemGraphToReactFlow(graph: ProblemGraph): { nodes: Node[]; edges: Edge[] } { + function getNodeId(node: ProblemNode) { + return node.problemType + node.solutionId.toString(); + } + + function getEdgeId(from: ProblemNode, to: ProblemNode) { + return getNodeId(from) + "-" + getNodeId(to); + } + + function getPositionX(levelInfo: LevelInfo | null) { + return levelInfo == null + ? 0 + : levelInfo.index * 200 - levelInfo.count * 50; + } + + function getPositionY(level: number) { + return level * 150; + } + + interface LevelInfo { + index: number; + count: number; + } + + let nodes: Node[] = []; + let edges: Edge[] = []; + + function addNode( + problemNode: ProblemNode, + level: number, + levelInfo: LevelInfo | null + ) { + let id = getNodeId(problemNode); + let label = + problemNode.problemType + + (problemNode.solver == null ? "" : " - " + problemNode.solver?.name); + let position = { + x: getPositionX(levelInfo), + y: getPositionY(level) + }; + let data = { label: label }; + let type: string; + if (level == 0) { + type = "input"; + } else if (problemNode.subRoutines.length == 0) { + type = "output"; + } else { + type = "default"; + } + + let node: Node = { + id: id, + data: data, + position: position, + type: type + }; + + if (problemNode.status == SolutionStatus.PENDING_USER_ACTION) { + node.style = { + background: "teal" + }; + + fetchSolvers(problemNode.problemType).then((solvers) => { + node.type = "default"; + for (let i = 0; i < solvers.length; i++) { + // Add new node manually + let solverId = solvers[i].id.toString(); + nodes.push({ + id: solverId, + data: { label: solvers[i].name }, + position: { + x: + node.position.x + + getPositionX({ index: i, count: solvers.length }), + y: getPositionY(level + 1) + }, + type: "output", + style: { + background: "teal" + } + }); + + edges.push({ + id: id + "-" + solverId, + type: "step", + source: id, + target: solverId + }); + } + node.style = { + background: "grey" + }; + + setNodes(nodes); + setEdges(edges); + }); + } + + nodes.push(node); + + for (let i = 0; i < problemNode.subRoutines.length; i++) { + const subRoutine = problemNode.subRoutines[i]; + edges.push({ + id: getEdgeId(problemNode, subRoutine), + source: id, + target: getNodeId(subRoutine), + animated: subRoutine.status == SolutionStatus.COMPUTING + }); + addNode(subRoutine, level + 1, { + index: i, + count: problemNode.subRoutines.length + }); + } + } + + addNode(graph.start, 0, null); + + return { nodes, edges }; + } + }, [props.graph, setEdges, setNodes]); + + return ( +
+ + + +
+ ); +}; \ No newline at end of file diff --git a/src/components/solvers/Graph/TestGraphDisplay.tsx b/src/components/solvers/Graph/TestGraphDisplay.tsx new file mode 100644 index 0000000..98c8452 --- /dev/null +++ b/src/components/solvers/Graph/TestGraphDisplay.tsx @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react"; +import { ProblemGraph } from "../../../api/data-model/ProblemGraph"; +import { fetchProblemGraph } from "../../../api/ToolboxAPI"; +import { ProblemGraphView } from "./ProblemGraphView"; + +export const TestGraphDisplay = () => { + let [graph, setGraph] = useState(null); + + useEffect(() => { + fetchProblemGraph() + .then(graph => { + setGraph(graph); + }); + }, []); + + return ( + ); +}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 54368ba..b895301 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -4,6 +4,7 @@ import Head from "next/head"; import { baseUrl } from "../api/ToolboxAPI"; import { ProblemChooser } from "../components/landing-page/ProblemChooser"; import { Layout } from "../components/layout/Layout"; +import { TestGraphDisplay } from "../components/solvers/Graph/TestGraphDisplay"; const Home: NextPage = () => { return ( @@ -14,6 +15,7 @@ const Home: NextPage = () => { {/* TODO: replace favicon */} + Welcome to the ProvideQ Toolbox!