diff --git a/package.json b/package.json index 0fd83d5..9a5b889 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,7 @@ "format": "prettier --write .", "test": "jest" }, - "resolutions": { - "d3-interpolate": "2.0.1" - }, "dependencies": { - "@antv/g6": "^4.7.16", "@chakra-ui/react": "^2.3.1", "@emotion/react": "11", "@emotion/styled": "11", @@ -26,7 +22,8 @@ "react-dom": "18.2.0", "react-icons": "^4.4.0", "react-multi-select-component": "^4.3.4", - "react-simple-code-editor": "^0.13.0" + "react-simple-code-editor": "^0.13.0", + "reactflow": "^11.10.3" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", diff --git a/src/api/ToolboxAPI.ts b/src/api/ToolboxAPI.ts index 53b3561..b856c01 100644 --- a/src/api/ToolboxAPI.ts +++ b/src/api/ToolboxAPI.ts @@ -1,66 +1,111 @@ -import { MetaSolverSetting } from "./data-model/MetaSolverSettings"; -import { ProblemSolver } from "./data-model/ProblemSolver"; -import { Solution } from "./data-model/Solution"; -import { SolutionStatus } from "./data-model/SolutionStatus"; -import { SolveRequest } from "./data-model/SolveRequest"; -import { SubRoutineDefinition } from "./data-model/SubRoutineDefinition"; +import { getInvalidProblemDto, ProblemDto } from "./data-model/ProblemDto"; +import { ProblemSolverInfo } from "./data-model/ProblemSolverInfo"; +import { ProblemState } from "./data-model/ProblemState"; +import { SubRoutineDefinitionDto } from "./data-model/SubRoutineDefinitionDto"; /** * Getter for the base url of the toolbox API. */ export const baseUrl = () => process.env.NEXT_PUBLIC_API_BASE_URL; +export async function fetchProblem( + problemTypeId: string, + problemId: string +): Promise> { + return fetch(`${baseUrl()}/problems/${problemTypeId}/${problemId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((json) => { + const data = json as ProblemDto; + + // Explicitly set solverId to undefined if it is null + if (data.solverId === null) { + data.solverId = undefined; + } + + return data; + }) + .catch((reason) => { + return { + ...getInvalidProblemDto(), + error: `${reason}`, + }; + }); +} + export async function postProblem( - problemType: string, - solveRequest: SolveRequest -): Promise { - return fetch(`${baseUrl()}/solve/${problemType}`, { + problemTypeId: string, + problemRequest: ProblemDto +): Promise> { + return fetch(`${baseUrl()}/problems/${problemTypeId}`, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(solveRequest), + body: JSON.stringify(problemRequest), + }) + .then((response) => response.json()) + .then((json) => json as ProblemDto) + .catch((reason) => { + return { + ...problemRequest, + error: `${reason}`, + }; + }); +} + +export async function patchProblem( + problemTypeId: string, + problemId: string, + updateParameters: { input?: any; solverId?: string; state?: ProblemState } +): Promise> { + let url = `${baseUrl()}/problems/${problemTypeId}/${problemId}`; + console.log(url); + return fetch(url, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updateParameters), }) .then((response) => response.json()) - .then((json) => json as Solution) + .then((json) => json as ProblemDto) .catch((reason) => { return { - id: -1, - status: SolutionStatus.INVALID, - solverName: "", - executionMilliseconds: 0, - solutionData: "", - debugData: "", - metaData: "", + ...getInvalidProblemDto(), error: `${reason}`, }; }); } export async function fetchSolvers( - problemType: string -): Promise { - return fetch(`${baseUrl()}/solvers/${problemType}`, { + problemTypeId: string +): Promise { + return fetch(`${baseUrl()}/solvers/${problemTypeId}`, { method: "GET", headers: { "Content-Type": "application/json", }, }) - .then((response) => response.json()) - .then((json) => json as ProblemSolver[]) + .then(async (response) => response.json()) + .then((json) => json as ProblemSolverInfo[]) .catch((reason) => { console.error(reason); - alert(`Could not retrieve solvers of type ${problemType}.`); + alert(`Could not retrieve solvers of type ${problemTypeId}.`); return []; }); } export async function fetchSubRoutines( - problemType: string, + problemTypeId: string, solverId: string -): Promise { +): Promise { return fetch( - `${baseUrl()}/sub-routines/${problemType}?${new URLSearchParams({ + `${baseUrl()}/sub-routines/${problemTypeId}?${new URLSearchParams({ id: solverId, })}`, { @@ -78,18 +123,18 @@ export async function fetchSubRoutines( }); } -export async function fetchMetaSolverSettings( - problemType: string -): Promise { - return fetch(`${baseUrl()}/meta-solver/settings/${problemType}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }) - .then((response) => response.json()) - .catch((reason) => { - console.log(reason); - return []; - }); -} +// export async function fetchMetaSolverSettings( +// problemTypeId: string +// ): Promise { +// return fetch(`${baseUrl()}/meta-solver/settings/${problemTypeId}`, { +// method: "GET", +// headers: { +// "Content-Type": "application/json", +// }, +// }) +// .then((response) => response.json()) +// .catch((reason) => { +// console.log(reason); +// return []; +// }); +// } diff --git a/src/api/data-model/ProblemDto.ts b/src/api/data-model/ProblemDto.ts new file mode 100644 index 0000000..5d59260 --- /dev/null +++ b/src/api/data-model/ProblemDto.ts @@ -0,0 +1,34 @@ +import { ProblemState } from "./ProblemState"; +import { getInvalidSolutionObject, SolutionObject } from "./SolutionObject"; +import { SubRoutineReferenceDto } from "./SubRoutineReferenceDto"; + +export interface ProblemDto { + id: string; + typeId: string; + input: T; + solution: SolutionObject; + state: ProblemState; + solverId?: string; + subProblems: SubRoutineReferenceDto[]; + error: string; +} + +export function getInvalidProblemDto(): ProblemDto { + return { + error: "", + id: "", + input: {} as T, + solution: getInvalidSolutionObject(), + solverId: "", + state: ProblemState.READY_TO_SOLVE, + subProblems: [], + typeId: "", + }; +} + +export function canProblemSolverBeUpdated(problem: ProblemDto): boolean { + return ( + problem.state === ProblemState.NEEDS_CONFIGURATION || + problem.state === ProblemState.READY_TO_SOLVE + ); +} diff --git a/src/api/data-model/ProblemSolver.ts b/src/api/data-model/ProblemSolver.ts deleted file mode 100644 index c682924..0000000 --- a/src/api/data-model/ProblemSolver.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ProblemSolver { - id: string; - name: string; -} diff --git a/src/api/data-model/ProblemSolverInfo.ts b/src/api/data-model/ProblemSolverInfo.ts new file mode 100644 index 0000000..dc83213 --- /dev/null +++ b/src/api/data-model/ProblemSolverInfo.ts @@ -0,0 +1,4 @@ +export interface ProblemSolverInfo { + id: string; + name: string; +} diff --git a/src/api/data-model/ProblemState.ts b/src/api/data-model/ProblemState.ts new file mode 100644 index 0000000..2f98e7f --- /dev/null +++ b/src/api/data-model/ProblemState.ts @@ -0,0 +1,6 @@ +export enum ProblemState { + NEEDS_CONFIGURATION = "NEEDS_CONFIGURATION", + READY_TO_SOLVE = "READY_TO_SOLVE", + SOLVING = "SOLVING", + SOLVED = "SOLVED", +} diff --git a/src/api/data-model/Solution.ts b/src/api/data-model/Solution.ts deleted file mode 100644 index ad9c508..0000000 --- a/src/api/data-model/Solution.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { SolutionStatus } from "./SolutionStatus"; - -export interface Solution { - id: number; - status: SolutionStatus; - solverName: string; - executionMilliseconds: number; - solutionData: any; - metaData: string; - debugData: string; - error: string; -} diff --git a/src/api/data-model/SolutionObject.ts b/src/api/data-model/SolutionObject.ts new file mode 100644 index 0000000..197ce88 --- /dev/null +++ b/src/api/data-model/SolutionObject.ts @@ -0,0 +1,26 @@ +import { SolutionStatus } from "./SolutionStatus"; + +export interface SolutionObject { + /** + * UUID of the solution. + */ + id: string; + status: SolutionStatus; + metaData: string; + solutionData: any; + debugData: string; + solverName: string; + executionMilliseconds: number; +} + +export function getInvalidSolutionObject(): SolutionObject { + return { + id: "", + status: SolutionStatus.INVALID, + metaData: "", + solutionData: undefined, + debugData: "", + solverName: "", + executionMilliseconds: -1, + }; +} diff --git a/src/api/data-model/SolutionStatus.ts b/src/api/data-model/SolutionStatus.ts index 8f65e4f..70bdb66 100644 --- a/src/api/data-model/SolutionStatus.ts +++ b/src/api/data-model/SolutionStatus.ts @@ -1,5 +1,6 @@ export enum SolutionStatus { - INVALID, - COMPUTING, - SOLVED, + INVALID = "INVALID", + COMPUTING = "COMPUTING", + SOLVED = "SOLVED", + ERROR = "ERROR", } diff --git a/src/api/data-model/SolveRequest.ts b/src/api/data-model/SolveRequest.ts deleted file mode 100644 index be08e54..0000000 --- a/src/api/data-model/SolveRequest.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MetaSolverSetting } from "./MetaSolverSettings"; - -export interface SolverChoice { - /** - * If no solver id is provided, the toolbox choose the solver itself via meta solver strategy - */ - requestedSolverId?: string; - /** - * If no solver id is provided, the toolbox choose the solver itself via meta solver strategy - */ - requestedMetaSolverSettings?: MetaSolverSetting[]; - /** - * Map from problem type to SubSolveRequest to allow explicitly requested solvers for a subroutine - */ - requestedSubSolveRequests: SolveMap; -} - -export interface SolveRequest extends SolverChoice { - requestContent: T; -} - -/** - * A SolveMap contains `SolverChoice` data for each problem type used in - * sub-routines of a request. - */ -export type SolveMap = { - [problemTypeId: string]: SolverChoice; -}; diff --git a/src/api/data-model/SubRoutineDefinition.ts b/src/api/data-model/SubRoutineDefinitionDto.ts similarity index 85% rename from src/api/data-model/SubRoutineDefinition.ts rename to src/api/data-model/SubRoutineDefinitionDto.ts index 8afafbc..109fbde 100644 --- a/src/api/data-model/SubRoutineDefinition.ts +++ b/src/api/data-model/SubRoutineDefinitionDto.ts @@ -2,12 +2,12 @@ * A sub-routine definition describes which problem type needs to be solved by a * sub-routine and why it needs to be solved. */ -export interface SubRoutineDefinition { +export interface SubRoutineDefinitionDto { /** * Identifies the type of problem that needs to be solved with the described * sub-routine. */ - type: string; + typeId: string; /** * Describes why this sub-routine is required to solve a bigger problem. */ diff --git a/src/api/data-model/SubRoutineReferenceDto.ts b/src/api/data-model/SubRoutineReferenceDto.ts new file mode 100644 index 0000000..c6f2b16 --- /dev/null +++ b/src/api/data-model/SubRoutineReferenceDto.ts @@ -0,0 +1,6 @@ +import { SubRoutineDefinitionDto } from "./SubRoutineDefinitionDto"; + +export interface SubRoutineReferenceDto { + subRoutine: SubRoutineDefinitionDto; + subProblemIds: string[]; +} diff --git a/src/components/landing-page/ProblemChooser.tsx b/src/components/landing-page/ProblemChooser.tsx index f2aec3e..086f673 100644 --- a/src/components/landing-page/ProblemChooser.tsx +++ b/src/components/landing-page/ProblemChooser.tsx @@ -16,7 +16,7 @@ export const ProblemChooser = (props: GridProps) => ( @@ -25,7 +25,7 @@ export const ProblemChooser = (props: GridProps) => ( @@ -34,10 +34,28 @@ export const ProblemChooser = (props: GridProps) => ( + + + + + + ); diff --git a/src/components/solvers/EditorControls.tsx b/src/components/solvers/EditorControls.tsx index f12df5e..c7d8994 100644 --- a/src/components/solvers/EditorControls.tsx +++ b/src/components/solvers/EditorControls.tsx @@ -5,7 +5,13 @@ import { Text, Tooltip, } from "@chakra-ui/react"; -import { TbDownload, TbHelp, TbUpload } from "react-icons/tb"; +import { + TbDownload, + TbHelp, + TbRepeat, + TbTrash, + TbUpload, +} from "react-icons/tb"; import { baseUrl } from "../../api/ToolboxAPI"; import { chooseFile } from "./FileInput"; @@ -25,9 +31,9 @@ export interface EditorControlsProps { */ editorContent: string; /** - * Contents of uploaded problem files will be sent to this handler. + * Function to set the contents of the editor that these controls relate to. */ - onUpload: (uploadContent: string) => void; + setEditorContent: (newContent: string) => void; /** * Link to the documentation for the problem type that is being edited. @@ -74,7 +80,7 @@ export const EditorControls = (props: EditorControlsProps) => { return ( {props.errorText ? ( - {props.errorText} + {props.errorText} ) : ( {props.idleText} )} @@ -90,7 +96,26 @@ export const EditorControls = (props: EditorControlsProps) => { } - onClick={() => upload(props.onUpload)} + onClick={() => upload(props.setEditorContent)} + /> + + + } + onClick={() => props.setEditorContent("")} + /> + + + } + onClick={() => { + props.setEditorContent(""); + setTimeout(() => { + props.setEditorContent(props.editorContent); + }); + }} /> diff --git a/src/components/solvers/Graph/GMLGraphView.tsx b/src/components/solvers/Graph/GMLGraphView.tsx new file mode 100644 index 0000000..55a10eb --- /dev/null +++ b/src/components/solvers/Graph/GMLGraphView.tsx @@ -0,0 +1,100 @@ +import { useEffect } from "react"; +import { + Controls, + Edge, + Node, + ReactFlow, + useEdgesState, + useNodesState, +} from "reactflow"; +import "reactflow/dist/style.css"; +import { parseGML } from "../../../converter/graph/gml/GmlParser"; + +export const GMLGraphView = (props: { gml: string }) => { + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + useEffect(() => { + let { nodes, edges } = gmlToReactFlow(props.gml); + setEdges(edges); + setNodes(nodes); + + function gmlToReactFlow(gml: string): { nodes: Node[]; edges: Edge[] } { + if (gml == null || gml == "") return { nodes: [], edges: [] }; + + let graph: any; + try { + graph = parseGML(gml); + } catch (e) { + return { nodes: [], edges: [] }; + } + + function getNodeType(node: any): string { + let input = false; + let output = false; + for (let edge of graph.edges) { + if (edge.source == node.id) { + input = true; + } + if (edge.target == node.id) { + output = true; + } + } + + if (input && output) { + return "default"; + } else if (input) { + return "input"; + } else if (output) { + return "output"; + } else { + return "default"; + } + } + + let nodes: Node[] = graph.nodes.map((node: any, i: number) => { + let n: Node = { + id: node.id, + data: { label: node.label }, + type: getNodeType(node), + position: { + x: 0, + y: i * 100, + }, + }; + return n; + }); + + let edges: Edge[] = graph.edges.map((edge: any, i: number) => { + let e: Edge = { + id: i.toString(), + source: edge.source, + target: edge.target, + label: edge.label, + }; + return e; + }); + + return { nodes, edges }; + } + }, [props.gml, setEdges, setNodes]); + + return ( +
+ + + +
+ ); +}; diff --git a/src/components/solvers/Graph/GraphArea.tsx b/src/components/solvers/Graph/GraphArea.tsx deleted file mode 100644 index a4176c9..0000000 --- a/src/components/solvers/Graph/GraphArea.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import G6, { Graph, GraphData } from "@antv/g6"; -import { Box, Center, Container, Select, Spacer } from "@chakra-ui/react"; -import { useEffect, useRef, useState } from "react"; - -interface GraphAreaProps { - graphData: GraphData; - - graphHeight: number; - graphWidth: number; -} - -export const GraphArea = (props: GraphAreaProps) => { - const graphTypes = [ - "random", - "force", - "circular", - "radial", - "mds", - "dagre", - "concentric", - "grid", - ]; - - const graphRef = useRef(null); - const [graph, setGraph] = useState(null); - const [graphDirected, setGraphDirected] = useState(false); - - useEffect(() => { - if (!graph && graphRef.current && graphRef.current.children.length == 0) { - setGraph( - new G6.Graph({ - container: graphRef.current, - width: 500, - height: 500, - modes: { - default: ["drag-canvas", "zoom-canvas", "drag-node"], - }, - defaultEdge: { - style: { - startArrow: true, - endArrow: true, - }, - }, - }) - ); - } - - if (graph && props.graphData) { - graph.data(props.graphData); - graph.render(); - updateGraphDirected(props.graphData.directed == "1" ?? false); - } - - function updateGraphDirected(directed: boolean) { - if (!graph || graphDirected == directed) return; - setGraphDirected(directed); - - if (directed) { - for (let edge of graph.getEdges()) { - graph.update(edge, { - style: { - startArrow: false, - endArrow: true, - }, - }); - } - } else { - for (let edge of graph.getEdges()) { - graph.update(edge, { - style: { - startArrow: true, - endArrow: true, - }, - }); - } - } - } - }, [graph, graphDirected, props.graphData]); - - return ( - - - - - -
- -
-
- ); -}; diff --git a/src/components/solvers/Graph/ProblemDetails.tsx b/src/components/solvers/Graph/ProblemDetails.tsx new file mode 100644 index 0000000..81f1fb7 --- /dev/null +++ b/src/components/solvers/Graph/ProblemDetails.tsx @@ -0,0 +1,85 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + Text, + Textarea, + VStack, +} from "@chakra-ui/react"; +import { ReactNode } from "react"; +import { ProblemDto } from "../../../api/data-model/ProblemDto"; +import { ProblemState } from "../../../api/data-model/ProblemState"; +import { SolutionView } from "../SolutionView"; +import { useSolvers } from "./SolverProvider"; + +function getHumanReadableState(state: ProblemState) { + switch (state) { + case ProblemState.NEEDS_CONFIGURATION: + return "Needs Configuration"; + case ProblemState.READY_TO_SOLVE: + return "Ready to Solve"; + case ProblemState.SOLVING: + return "Solving"; + case ProblemState.SOLVED: + return "Solved"; + } +} + +function getAccordionItem(label: string, content: ReactNode) { + return ( + +

+ + + {label} + + + +

+ {content} +
+ ); +} + +export const ProblemDetails = (props: { problemDto: ProblemDto }) => { + const { solvers, getSolvers } = useSolvers(); + + // Update solvers in case they are not loaded yet + if (!solvers[props.problemDto.typeId]) getSolvers(props.problemDto.typeId); + + return ( + +