diff --git a/frontend/src/modules/CalibrationGraph/CalibrationGraph.module.scss b/frontend/src/modules/CalibrationGraph/CalibrationGraph.module.scss new file mode 100644 index 00000000..7e1a2b19 --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/CalibrationGraph.module.scss @@ -0,0 +1,32 @@ +@import "../../assets/styles/variables"; +@import "../../assets/styles/base"; + +.wrapper { + height: 100%; + display: flex; + color: var(--font) !important; +} + +.nodesContainer { + display: flex; + flex-direction: column; + overflow: hidden; + flex-grow: 1; + margin-left: 20px; + margin-right: 20px; + margin-bottom: 10px; + border-radius: var(--border-radius); +} + +.titleWrapper { + justify-content: space-between; +} + +.listWrapper { + flex-grow: 1; + border: 2px solid var(--border-color); + padding: 2px;; + background-color: var(--popup-background); + max-width: 100%; + overflow: scroll; +} diff --git a/frontend/src/modules/CalibrationGraph/api/CalibrationsAPI.tsx b/frontend/src/modules/CalibrationGraph/api/CalibrationsAPI.tsx new file mode 100644 index 00000000..0cc5721c --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/api/CalibrationsAPI.tsx @@ -0,0 +1,36 @@ +import Api, { BASIC_HEADERS } from "../../../utils/api"; +import { Res } from "../../../common/interfaces/Api"; +import { ALL_GRAPHS, GET_WORKFLOW_GRAPH, SUBMIT_WORKFLOW_RUN } from "../../../utils/api/apiRoutes"; +import { API_METHODS } from "../../../common/enums/Api"; + +export class CalibrationsApi extends Api { + constructor() { + super(); + } + + static api(path: string): string { + return this.address + path; + } + + static fetchAllGraphs(rescan: boolean = false): Promise> { + return this._fetch(this.api(ALL_GRAPHS()), API_METHODS.GET, { + headers: BASIC_HEADERS, + queryParams: { rescan }, + }); + } + + static fetchGraph(name: string): Promise> { + return this._fetch(this.api(GET_WORKFLOW_GRAPH()), API_METHODS.GET, { + headers: BASIC_HEADERS, + queryParams: { name }, + }); + } + + static submitWorkflow(name: string, workflow: unknown): Promise> { + return this._fetch(this.api(SUBMIT_WORKFLOW_RUN()), API_METHODS.POST, { + headers: BASIC_HEADERS, + body: JSON.stringify(workflow), + queryParams: { name }, + }); + } +} diff --git a/frontend/src/modules/CalibrationGraph/components/CalibrationGraphElement/CalibrationGraphElement.module.scss b/frontend/src/modules/CalibrationGraph/components/CalibrationGraphElement/CalibrationGraphElement.module.scss new file mode 100644 index 00000000..e2572e9f --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/components/CalibrationGraphElement/CalibrationGraphElement.module.scss @@ -0,0 +1,73 @@ +.searchContainer { + max-width: 280px; + padding-bottom: 20px; + + input { + border-radius: 25px; + font-style: italic; + } +} + +.wrapper { + padding-top: 13px; + padding-left: 25px; + padding-bottom: 13px; + width: auto; + min-height: 30px; + border: solid 1px var(--border-color); + border-radius: 7px; + flex-direction: column; +} + +.upperContainer { + display: flex; +} + +.bottomContainer { + display: flex; +} + +.parametersContainer { + max-width: 40%; +} + +.graphContainer { + flex: 1; + align-content: center; + //text-align: center; + width: 60%; +} + +.leftContainer { + align-content: center; + display: flex; + gap: 10px; + text-wrap: nowrap; +} + +.rightContainer { + align-content: center; + text-align: right; + flex-grow: 1; + padding-right: 10px; +} + +.iconWrapper { + align-content: center; + max-height: fit-content; + padding-right: 10px; + + svg { + display: table-row; + + } +} + +.titleWrapper { + align-content: center; + color: var(--blue-icon-button-text); +} + +.calibrationGraphSelected { + background-color: var(--selected-node); +} \ No newline at end of file diff --git a/frontend/src/modules/CalibrationGraph/components/CalibrationGraphElement/CalibrationGraphElement.tsx b/frontend/src/modules/CalibrationGraph/components/CalibrationGraphElement/CalibrationGraphElement.tsx new file mode 100644 index 00000000..00685999 --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/components/CalibrationGraphElement/CalibrationGraphElement.tsx @@ -0,0 +1,146 @@ +import React from "react"; +// eslint-disable-next-line css-modules/no-unused-class +import styles from "../CalibrationGraphElement/CalibrationGraphElement.module.scss"; +import { PlayIcon } from "../../../../ui-lib/Icons/PlayIcon"; +import { classNames } from "../../../../utils/classnames"; +import { InputParameter, Parameters, SingleParameter } from "../../../common/Parameters"; +import { CalibrationGraphWorkflow } from "../CalibrationGraphList"; +import { useSelectionContext } from "../../../common/context/SelectionContext"; +import { Checkbox } from "@mui/material"; +import InputField from "../../../../DEPRECATED_components/common/Input/InputField"; +import { ParameterList } from "../../../common/ParameterList"; +import { useCalibrationGraphContext } from "../../context/CalibrationGraphContext"; +import CytoscapeGraph from "../CytoscapeGraph/CytoscapeGraph"; +import { CalibrationsApi } from "../../api/CalibrationsAPI"; +import { NodeDTO } from "../../../Nodes/components/NodeElement/NodeElement"; + +export interface ICalibrationGraphElementProps { + calibrationGraphKey?: string; + calibrationGraph: CalibrationGraphWorkflow; +} + +interface TransformedGraph { + parameters: { [key: string]: string | number }; + nodes: { [key: string]: { parameters: InputParameter } }; +} + +export const CalibrationGraphElement: React.FC = ({ calibrationGraphKey, calibrationGraph }) => { + const { selectedItemName, setSelectedItemName } = useSelectionContext(); + const { workflowGraphElements, setSelectedWorkflowName, allCalibrationGraphs, setAllCalibrationGraphs, selectedWorkflowName } = + useCalibrationGraphContext(); + + const updateParameter = (paramKey: string, newValue: boolean | number | string, workflow?: NodeDTO | CalibrationGraphWorkflow) => { + const updatedParameters = { + ...workflow?.parameters, + [paramKey]: { + ...(workflow?.parameters as InputParameter)[paramKey], + default: newValue, + }, + }; + + if (selectedWorkflowName && allCalibrationGraphs?.[selectedWorkflowName]) { + const updatedWorkflow = { + ...allCalibrationGraphs[selectedWorkflowName], + parameters: updatedParameters, + }; + + const updatedCalibrationGraphs = { + ...allCalibrationGraphs, + [selectedWorkflowName]: updatedWorkflow, + }; + + setAllCalibrationGraphs(updatedCalibrationGraphs); + } + }; + const getInputElement = (key: string, parameter: SingleParameter, node?: NodeDTO | CalibrationGraphWorkflow) => { + switch (parameter.type) { + case "boolean": + return ( + updateParameter(key, !parameter.default, node)} + inputProps={{ "aria-label": "controlled" }} + /> + ); + default: + return ( + { + updateParameter(key, val, node); + }} + /> + ); + } + }; + const transformDataForSubmit = () => { + const input = allCalibrationGraphs?.[selectedWorkflowName ?? ""]; + const workflowParameters = input?.parameters; + const transformParameters = (params?: InputParameter) => { + let transformedParams = {}; + for (const key in params) { + transformedParams = { ...transformedParams, [key]: params[key].default }; + } + return transformedParams; + }; + + const transformedGraph: TransformedGraph = { + parameters: transformParameters(workflowParameters), + nodes: {}, + }; + + for (const nodeKey in input?.nodes) { + const node = input?.nodes[nodeKey]; + transformedGraph.nodes[nodeKey] = { + parameters: transformParameters(node.parameters), + }; + } + + return transformedGraph; + }; + + const handleSubmit = async () => { + if (selectedWorkflowName) { + CalibrationsApi.submitWorkflow(selectedWorkflowName, transformDataForSubmit()); + } + }; + + const show = selectedItemName === calibrationGraphKey; + return ( +
{ + setSelectedItemName(calibrationGraphKey); + setSelectedWorkflowName(calibrationGraphKey); + }} + > +
+
+
+
+ +
+
+
{calibrationGraphKey}
+
+
+
{calibrationGraph?.description}
+
+
+
+
+ + +
+
{show && workflowGraphElements && }
+
+
+ ); +}; diff --git a/frontend/src/modules/CalibrationGraph/components/CalibrationGraphElement/CalibrationGraphSearch.tsx b/frontend/src/modules/CalibrationGraph/components/CalibrationGraphElement/CalibrationGraphSearch.tsx new file mode 100644 index 00000000..3a314ae5 --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/components/CalibrationGraphElement/CalibrationGraphSearch.tsx @@ -0,0 +1,14 @@ +import React from "react"; +// eslint-disable-next-line css-modules/no-unused-class +import styles from "../CalibrationGraphElement/CalibrationGraphElement.module.scss"; +import InputField from "../../../../DEPRECATED_components/common/Input/InputField"; +import { SearchIcon } from "../../../../ui-lib/Icons/SearchIcon"; +import { IconType } from "../../../../common/interfaces/InputProps"; + +export const CalibrationGraphSearch: React.FC = () => { + return ( +
+ }> +
+ ); +}; diff --git a/frontend/src/modules/CalibrationGraph/components/CalibrationGraphList.tsx b/frontend/src/modules/CalibrationGraph/components/CalibrationGraphList.tsx new file mode 100644 index 00000000..4618fccc --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/components/CalibrationGraphList.tsx @@ -0,0 +1,28 @@ +import React from "react"; +// eslint-disable-next-line css-modules/no-unused-class +import styles from "../CalibrationGraph.module.scss"; +import { CalibrationGraphElement } from "./CalibrationGraphElement/CalibrationGraphElement"; +import { NodeMap } from "../../Nodes/components/NodeElement/NodeElement"; +import { useCalibrationGraphContext } from "../context/CalibrationGraphContext"; +import { InputParameter } from "../../common/Parameters"; + +export interface CalibrationGraphWorkflow { + name?: string; + title?: string; + description: string; + parameters?: InputParameter; + nodes?: NodeMap; + connectivity?: string[][]; +} + +export const CalibrationGraphList: React.FC = () => { + const { allCalibrationGraphs } = useCalibrationGraphContext(); + if (!allCalibrationGraphs || Object.entries(allCalibrationGraphs).length === 0) return
No calibration graphs
; + return ( +
+ {Object.entries(allCalibrationGraphs ?? {}).map(([key, graph]) => { + return ; + })} +
+ ); +}; diff --git a/frontend/src/modules/CalibrationGraph/components/CytoscapeGraph/CytoscapeGraph.module.scss b/frontend/src/modules/CalibrationGraph/components/CytoscapeGraph/CytoscapeGraph.module.scss new file mode 100644 index 00000000..027542b3 --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/components/CytoscapeGraph/CytoscapeGraph.module.scss @@ -0,0 +1,11 @@ +@import "../../../../assets/styles/variables"; +@import "../../../../assets/styles/base"; + +.wrapper { + width: auto; + height: 100%; + background: var(--background-color); + border-radius: var(--border-radius); + margin-left: 10px; + margin-right: 10px; +} diff --git a/frontend/src/modules/CalibrationGraph/components/CytoscapeGraph/CytoscapeGraph.tsx b/frontend/src/modules/CalibrationGraph/components/CytoscapeGraph/CytoscapeGraph.tsx new file mode 100644 index 00000000..4bf05e1f --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/components/CytoscapeGraph/CytoscapeGraph.tsx @@ -0,0 +1,107 @@ +import cytoscape, { ElementDefinition, EventObject } from "cytoscape"; +import { useEffect, useRef } from "react"; +import { CytoscapeLayout } from "./config/Cytoscape"; + +import styles from "./CytoscapeGraph.module.scss"; +import { useCalibrationGraphContext } from "../../context/CalibrationGraphContext"; + +cytoscape.warnings(false); + +interface IProps { + elements: ElementDefinition[]; +} + +export default function CytoscapeGraph({ elements }: IProps) { + const { setSelectedNodeNameInWorkflow } = useCalibrationGraphContext(); + const cy = useRef(); + const divRef = useRef(null); + + const style = [ + { + selector: "node", + style: { + "background-color": "#ffffff", + label: "data(id)", + width: "50px", + height: "50px", + "border-width": "2px", + "border-color": "#000", + color: "#a8a6a6", + }, + }, + { + selector: ":selected", + css: { + "background-color": "#3b93dc", + "border-width": "1px", + "text-outline-width": 0.3, + "font-weight": 1, + "font-color": "#ffffff", + }, + }, + { + selector: "edge", + style: { + width: 3, + "line-color": "#cbc4c4", + "target-arrow-color": "#cbc4c4", + "target-arrow-shape": "triangle", + "curve-style": "bezier", + "font-color": "#c9bcbc", + }, + }, + ]; + useEffect(() => { + if (elements) { + if (!cy.current) { + cy.current = cytoscape({ + container: divRef.current, + elements, + style, + layout: CytoscapeLayout, + zoom: 1, + minZoom: 0.1, + maxZoom: 1.6, + wheelSensitivity: 0.1, + }); + } else { + // update style around node if its status is changed + cy.current.batch(() => { + const allElements = cy.current?.elements() ?? []; + allElements.forEach((element) => { + const newElement = elements?.find((s) => s.data.id === element.id()); + if (newElement) { + element.classes(newElement.classes); + } + }); + }); + } + } + }, [elements]); + + useEffect(() => { + const onClickN = (e: EventObject) => { + setSelectedNodeNameInWorkflow((e.target.data() as { id: string }).id); + }; + cy.current?.nodes().on("click", onClickN); + + return () => { + cy.current?.nodes().off("click", "node"); + }; + }, [setSelectedNodeNameInWorkflow, cy.current]); + + useEffect(() => { + const onClick = (e: EventObject) => { + if (e.target === cy.current) { + setSelectedNodeNameInWorkflow(undefined); + } + }; + cy.current?.on("click", onClick); + + return () => { + cy.current?.off("click", onClick); + }; + }, [setSelectedNodeNameInWorkflow, cy.current]); + + return
; +} diff --git a/frontend/src/modules/CalibrationGraph/components/CytoscapeGraph/config/Cytoscape.ts b/frontend/src/modules/CalibrationGraph/components/CytoscapeGraph/config/Cytoscape.ts new file mode 100644 index 00000000..d0cca7fd --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/components/CytoscapeGraph/config/Cytoscape.ts @@ -0,0 +1,7 @@ +export const CytoscapeLayout = { + name: "breadthfirst", + fit: true, + directed: true, + padding: 30, + spacingFactor: 2.4, +}; diff --git a/frontend/src/modules/CalibrationGraph/context/CalibrationGraphContext.tsx b/frontend/src/modules/CalibrationGraph/context/CalibrationGraphContext.tsx new file mode 100644 index 00000000..63df4935 --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/context/CalibrationGraphContext.tsx @@ -0,0 +1,99 @@ +import React, { Dispatch, SetStateAction, useContext, useEffect, useState } from "react"; +import noop from "../../../common/helpers"; +import { CalibrationGraphWorkflow } from "../components/CalibrationGraphList"; +import { CalibrationsApi } from "../api/CalibrationsAPI"; +import { ElementDefinition } from "cytoscape"; + +interface CalibrationGraphProviderProps { + children: React.JSX.Element; +} + +export interface CalibrationGraphMap { + [key: string]: CalibrationGraphWorkflow; +} + +interface ICalibrationGraphContext { + allCalibrationGraphs?: CalibrationGraphMap; + setAllCalibrationGraphs: (array: CalibrationGraphMap | undefined) => void; + selectedWorkflow?: CalibrationGraphWorkflow; + setSelectedWorkflow: (workflow: CalibrationGraphWorkflow) => void; + selectedWorkflowName?: string; + setSelectedWorkflowName: (workflowName: string | undefined) => void; + selectedNodeNameInWorkflow?: string; + setSelectedNodeNameInWorkflow: (nodeName: string | undefined) => void; + workflowGraphElements?: ElementDefinition[]; + setWorkflowGraphElements: Dispatch>; +} + +const CalibrationGraphContext = React.createContext({ + allCalibrationGraphs: undefined, + setAllCalibrationGraphs: noop, + + selectedWorkflow: undefined, + setSelectedWorkflow: noop, + + selectedWorkflowName: undefined, + setSelectedWorkflowName: noop, + + selectedNodeNameInWorkflow: undefined, + setSelectedNodeNameInWorkflow: noop, + + workflowGraphElements: undefined, + setWorkflowGraphElements: noop, +}); + +export const useCalibrationGraphContext = () => useContext(CalibrationGraphContext); + +export const CalibrationGraphContextProvider = (props: CalibrationGraphProviderProps): React.ReactElement => { + const [allCalibrationGraphs, setAllCalibrationGraphs] = useState(undefined); + const [selectedWorkflow, setSelectedWorkflow] = useState(undefined); + const [selectedWorkflowName, setSelectedWorkflowName] = useState(undefined); + const [selectedNodeNameInWorkflow, setSelectedNodeNameInWorkflow] = useState(undefined); + const [workflowGraphElements, setWorkflowGraphElements] = useState(undefined); + + const fetchAllCalibrationGraphs = async () => { + const response = await CalibrationsApi.fetchAllGraphs(); + if (response.isOk) { + setAllCalibrationGraphs(response.result! as CalibrationGraphMap); + } else if (response.error) { + console.log(response.error); + } + }; + const fetchWorkflowGraph = async (nodeName: string) => { + const response = await CalibrationsApi.fetchGraph(nodeName); + if (response.isOk) { + setWorkflowGraphElements(response.result! as ElementDefinition[]); + } else if (response.error) { + console.log(response.error); + } + }; + useEffect(() => { + fetchAllCalibrationGraphs(); + }, []); + + useEffect(() => { + if (selectedWorkflowName) { + fetchWorkflowGraph(selectedWorkflowName); + setSelectedWorkflow(allCalibrationGraphs?.[selectedWorkflowName]); + } + }, [selectedWorkflowName]); + + return ( + + {props.children} + + ); +}; diff --git a/frontend/src/modules/CalibrationGraph/index.tsx b/frontend/src/modules/CalibrationGraph/index.tsx new file mode 100644 index 00000000..18fb254c --- /dev/null +++ b/frontend/src/modules/CalibrationGraph/index.tsx @@ -0,0 +1,31 @@ +import React from "react"; +// eslint-disable-next-line css-modules/no-unused-class +import styles from "../CalibrationGraph/CalibrationGraph.module.scss"; +import { CalibrationGraphContextProvider } from "./context/CalibrationGraphContext"; +import PageName from "../../DEPRECATED_components/common/Page/PageName"; +import { CalibrationGraphList } from "./components/CalibrationGraphList"; +import { CalibrationGraphSearch } from "./components/CalibrationGraphElement/CalibrationGraphSearch"; +import { SelectionContextProvider } from "../common/context/SelectionContext"; + +const CalibrationGraph = () => { + const heading = "Run calibration graph"; + return ( +
+
+
+ {heading} +
+ + +
+
+ ); +}; + +export default () => ( + + + + + +); diff --git a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.module.scss b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.module.scss index b6e07921..cfe292a0 100644 --- a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.module.scss +++ b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.module.scss @@ -5,8 +5,8 @@ border: 2px solid var(--border-color); border-radius: 7px; padding: 10px; + margin-top: 2px; margin-bottom: 2px; - } .nodeSelected { @@ -29,56 +29,6 @@ display: inline-block; } -.parameterTitle { - font-size: 14px; - height: 25px; - display: flex; -} - -.parametersWrapper { - gap: 10px; - font-size: 12px; - padding-left: 15px; -} - -.nodeNotSelected { - display: none; -} - -.parameterValues { - display: flex; - width: 100%; -} - -.parameterLabel { - padding-top: 10px; - padding-bottom: 10px; - padding-left: 20px; - align-content: center; - width: 60%; -} - -.parameterValue { - width: 100%; - margin-top: 2px; - max-height: 25px; - - div, input { - height: 90%; - color: var(--font) !important; - border-color: var(--box-background) !important; - } - - span { - padding: 0; - align-self: center; - } - - fieldset { - border-color: var(--box-background) !important; - } -} - .titleOrName { display: flex; flex: 1; diff --git a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx index 3a63d435..fe2ed628 100644 --- a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx +++ b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx @@ -6,23 +6,15 @@ import { Checkbox, CircularProgress } from "@mui/material"; import { useNodesContext } from "../../context/NodesContext"; import { classNames } from "../../../../utils/classnames"; import { NodesApi } from "../../api/NodesAPI"; - -export interface SingleParameter { - default?: string | boolean | number; - title: string; - type: string; - // isFocused?: boolean; -} - -export interface InputParameter { - [key: string]: SingleParameter; -} +import { InputParameter, Parameters, SingleParameter } from "../../../common/Parameters"; +import { useSelectionContext } from "../../../common/context/SelectionContext"; export interface NodeDTO { name: string; title?: string; description: string; - input_parameters: InputParameter; + parameters?: InputParameter; + nodes?: InputParameter; } export interface NodeMap { @@ -30,19 +22,18 @@ export interface NodeMap { } export const NodeElement: React.FC<{ nodeKey: string; node: NodeDTO }> = ({ nodeKey, node }) => { - const [expanded] = React.useState(true); - const { selectedNode, setSelectedNode, isNodeRunning, setRunningNodeInfo, setIsNodeRunning, setRunningNode, allNodes, setAllNodes } = - useNodesContext(); + const { selectedItemName, setSelectedItemName } = useSelectionContext(); + const { isNodeRunning, setRunningNodeInfo, setIsNodeRunning, setRunningNode, allNodes, setAllNodes } = useNodesContext(); const updateParameter = (paramKey: string, newValue: boolean | number | string) => { const updatedParameters = { - ...node.input_parameters, + ...node.parameters, [paramKey]: { - ...node.input_parameters[paramKey], + ...(node.parameters as InputParameter)[paramKey], default: newValue, }, }; - setAllNodes({ ...allNodes, [nodeKey]: { ...node, input_parameters: updatedParameters } }); + setAllNodes({ ...allNodes, [nodeKey]: { ...node, parameters: updatedParameters } }); }; const getInputElement = (key: string, parameter: SingleParameter) => { switch (parameter.type) { @@ -91,13 +82,15 @@ export const NodeElement: React.FC<{ nodeKey: string; node: NodeDTO }> = ({ node setIsNodeRunning(true); setRunningNode(node); setRunningNodeInfo({ timestampOfRun: formatDate(new Date()), status: "running" }); - NodesApi.submitNodeParameters(node.name, transformInputParameters(node.input_parameters)); + NodesApi.submitNodeParameters(node.name, transformInputParameters(node.parameters as InputParameter)); }; return (
setSelectedNode(node)} + className={classNames(styles.rowWrapper, selectedItemName === node.name && styles.nodeSelected)} + onClick={() => { + setSelectedItemName(node.name); + }} >
@@ -106,36 +99,27 @@ export const NodeElement: React.FC<{ nodeKey: string; node: NodeDTO }> = ({ node
{node.description}
- {isNodeRunning && node.name === selectedNode?.name && } - {isNodeRunning && node.name !== selectedNode?.name && ( + {isNodeRunning && node.name === selectedItemName && } + {isNodeRunning && node.name !== selectedItemName && ( handleClick()}> Run )} {!isNodeRunning && ( - handleClick()}> + handleClick()}> Run )}
-
- {Object.entries(node.input_parameters).length > 0 && ( -
- {/*
setExpanded(!expanded)}>*/} - {/* */} - {/*
*/} - Parameters -
- )} - {expanded && - Object.entries(node.input_parameters).map(([key, parameter]) => ( -
-
{parameter.title}:
-
{getInputElement(key, parameter)}
-
- ))} -
+
); }; diff --git a/frontend/src/modules/Nodes/components/RunningJob/RunningJob.tsx b/frontend/src/modules/Nodes/components/RunningJob/RunningJob.tsx index 3b8f5833..949c1017 100644 --- a/frontend/src/modules/Nodes/components/RunningJob/RunningJob.tsx +++ b/frontend/src/modules/Nodes/components/RunningJob/RunningJob.tsx @@ -123,7 +123,7 @@ export const RunningJob: React.FC = () => { const getRunningJobParameters = () => { return ( <> - {Object.entries(runningNode?.input_parameters ?? {}).length > 0 && ( + {Object.entries(runningNode?.parameters ?? {}).length > 0 && (
{/*
setExpanded(!expanded)}>*/} @@ -134,7 +134,7 @@ export const RunningJob: React.FC = () => {
{ // expanded && - Object.entries(runningNode?.input_parameters ?? {}).map(([key, parameter]) => ( + Object.entries(runningNode?.parameters ?? {}).map(([key, parameter]) => (
{parameter.title}:
{parameter.default?.toString()}
diff --git a/frontend/src/modules/Nodes/context/NodesContext.tsx b/frontend/src/modules/Nodes/context/NodesContext.tsx index f284805b..88e0dc25 100644 --- a/frontend/src/modules/Nodes/context/NodesContext.tsx +++ b/frontend/src/modules/Nodes/context/NodesContext.tsx @@ -27,8 +27,6 @@ export interface RunningNodeInfo { } interface INodesContext { - selectedNode?: NodeDTO; - setSelectedNode: (selectedNode: NodeDTO) => void; runningNode?: NodeDTO; runningNodeInfo?: RunningNodeInfo; setRunningNode: (selectedNode: NodeDTO) => void; @@ -43,8 +41,6 @@ interface INodesContext { } const NodesContext = React.createContext({ - selectedNode: undefined, - setSelectedNode: noop, runningNode: undefined, runningNodeInfo: undefined, setRunningNode: noop, @@ -80,7 +76,6 @@ interface NodeStatusResponseType { export function NodesContextProvider(props: NodesContextProviderProps): React.ReactElement { const [allNodes, setAllNodes] = useState(undefined); - const [selectedNode, setSelectedNode] = useState(undefined); const [runningNode, setRunningNode] = useState(undefined); const [runningNodeInfo, setRunningNodeInfo] = useState(undefined); const [isNodeRunning, setIsNodeRunning] = useState(false); @@ -107,9 +102,9 @@ export function NodesContextProvider(props: NodesContextProviderProps): React.Re const fetchNodeResults = async () => { const lastRunResponse = await NodesApi.fetchLastRunInfo(); - if (lastRunResponse.isOk) { + if (lastRunResponse?.isOk) { const lastRunResponseResult = lastRunResponse.result as NodeStatusResponseType; - if (lastRunResponseResult.status !== "error") { + if (lastRunResponseResult && lastRunResponseResult.status !== "error") { const idx = lastRunResponseResult.idx.toString(); if (lastRunResponseResult.idx) { const snapshotResponse = await SnapshotsApi.fetchSnapshotResult(idx); @@ -153,7 +148,6 @@ export function NodesContextProvider(props: NodesContextProviderProps): React.Re useEffect(() => { if (!isNodeRunning) { - console.log("fetchNodeResults", isNodeRunning); fetchNodeResults(); } }, [isNodeRunning]); @@ -173,8 +167,6 @@ export function NodesContextProvider(props: NodesContextProviderProps): React.Re return ( { const heading = "Run calibration node"; @@ -31,6 +32,8 @@ const NodesPage = () => { export default () => ( - + + + ); diff --git a/frontend/src/modules/common/ParameterList.tsx b/frontend/src/modules/common/ParameterList.tsx new file mode 100644 index 00000000..95c80a40 --- /dev/null +++ b/frontend/src/modules/common/ParameterList.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { NodeDTO, NodeMap } from "../Nodes/components/NodeElement/NodeElement"; +import { InputParameter, Parameters, SingleParameter } from "./Parameters"; +import { Checkbox } from "@mui/material"; +import InputField from "../../DEPRECATED_components/common/Input/InputField"; +import { useCalibrationGraphContext } from "../CalibrationGraph/context/CalibrationGraphContext"; +import { CalibrationGraphWorkflow } from "../CalibrationGraph/components/CalibrationGraphList"; + +interface IProps { + showParameters: boolean; + mapOfItems?: NodeMap; +} + +export const ParameterList: React.FC = ({ showParameters = false, mapOfItems }) => { + const { allCalibrationGraphs, setAllCalibrationGraphs, selectedWorkflowName } = useCalibrationGraphContext(); + const updateParameter = (paramKey: string, newValue: boolean | number | string, node?: NodeDTO | CalibrationGraphWorkflow) => { + const updatedParameters = { + ...node?.parameters, + [paramKey]: { + ...(node?.parameters as InputParameter)[paramKey], + default: newValue, + }, + }; + const changedNode = { ...(node as NodeDTO), parameters: updatedParameters as InputParameter }; + const nodeName = node?.name; + if (nodeName && selectedWorkflowName && allCalibrationGraphs?.[selectedWorkflowName]) { + const changedNodeSInWorkflow = { + ...allCalibrationGraphs[selectedWorkflowName].nodes, + [nodeName]: changedNode, + }; + + const updatedWorkflow = { + ...allCalibrationGraphs[selectedWorkflowName], + nodes: changedNodeSInWorkflow, + }; + + const updatedCalibrationGraphs = { + ...allCalibrationGraphs, + [selectedWorkflowName]: updatedWorkflow, + }; + + setAllCalibrationGraphs(updatedCalibrationGraphs); + } + }; + + const getInputElement = (key: string, parameter: SingleParameter, node?: NodeDTO | CalibrationGraphWorkflow) => { + switch (parameter.type) { + case "boolean": + return ( + updateParameter(key, !parameter.default, node)} + inputProps={{ "aria-label": "controlled" }} + /> + ); + default: + return ( + { + updateParameter(key, val, node); + }} + /> + ); + } + }; + return ( + <> + {Object.entries(mapOfItems ?? {}).map(([key, parameter]) => { + return ( + + ); + })} + + ); +}; diff --git a/frontend/src/modules/common/Parameters.module.scss b/frontend/src/modules/common/Parameters.module.scss new file mode 100644 index 00000000..128a3190 --- /dev/null +++ b/frontend/src/modules/common/Parameters.module.scss @@ -0,0 +1,54 @@ +.parametersWrapper { + gap: 10px; + font-size: 12px; + padding-left: 15px; + padding-top: 10px; +} + +.arrowIconWrapper { + margin-top: -3px; + padding-right: 5px; +} + +.nodeNotSelected { + display: none; +} + +.parameterTitle { + font-size: 14px; + height: 25px; + display: flex; +} + +.parameterValues { + display: flex; + width: 100%; +} + +.parameterLabel { + padding: 10px 20px; + align-content: center; + text-wrap: nowrap; + //width: 60%; +} + +.parameterValue { + width: 100%; + margin-top: 2px; + max-height: 25px; + + div, input { + height: 90%; + color: var(--font) !important; + border-color: var(--box-background) !important; + } + + span { + padding: 0; + align-self: center; + } + + fieldset { + border-color: var(--box-background) !important; + } +} diff --git a/frontend/src/modules/common/Parameters.tsx b/frontend/src/modules/common/Parameters.tsx new file mode 100644 index 00000000..754dbe6b --- /dev/null +++ b/frontend/src/modules/common/Parameters.tsx @@ -0,0 +1,71 @@ +import React, { useEffect } from "react"; +import { classNames } from "../../utils/classnames"; +import styles from "./Parameters.module.scss"; +import { NodeDTO } from "../Nodes/components/NodeElement/NodeElement"; +import { ArrowIcon } from "../../ui-lib/Icons/ArrowIcon"; +import { CalibrationGraphWorkflow } from "../CalibrationGraph/components/CalibrationGraphList"; +import { useCalibrationGraphContext } from "../CalibrationGraph/context/CalibrationGraphContext"; + +interface IProps { + parametersExpanded?: boolean; + show: boolean; + showTitle: boolean; + title?: string; + currentItem?: NodeDTO | CalibrationGraphWorkflow; + getInputElement: (key: string, parameter: SingleParameter, node?: NodeDTO | CalibrationGraphWorkflow) => React.JSX.Element; +} + +export interface SingleParameter { + default?: string | boolean | number; + title: string; + type: string; +} + +export interface InputParameter { + [key: string]: SingleParameter; +} + +export const Parameters: React.FC = ({ + parametersExpanded = false, + show = false, + showTitle = true, + title, + currentItem, + getInputElement, +}) => { + const { selectedNodeNameInWorkflow } = useCalibrationGraphContext(); + const [expanded, setExpanded] = React.useState(selectedNodeNameInWorkflow === title ?? parametersExpanded); + + useEffect(() => { + if (selectedNodeNameInWorkflow === title) { + setExpanded(true); + } else { + setExpanded(false); + } + }, [selectedNodeNameInWorkflow]); + + return ( +
+ {showTitle && Object.entries(currentItem?.parameters ?? {}).length > 0 && ( +
+
{ + setExpanded(!expanded); + }} + > + +
+ {title ?? "Parameters"} +
+ )} + {expanded && + Object.entries(currentItem?.parameters ?? {}).map(([key, parameter]) => ( +
+
{parameter.title}:
+
{getInputElement(key, parameter, currentItem)}
+
+ ))} +
+ ); +}; diff --git a/frontend/src/modules/common/context/SelectionContext.tsx b/frontend/src/modules/common/context/SelectionContext.tsx new file mode 100644 index 00000000..12db120e --- /dev/null +++ b/frontend/src/modules/common/context/SelectionContext.tsx @@ -0,0 +1,33 @@ +import React, { useContext, useState } from "react"; +import noop from "../../../common/helpers"; + +interface ISelectionContext { + selectedItemName?: string; + setSelectedItemName: (selectedNode: string | undefined) => void; +} + +const SelectionContext = React.createContext({ + selectedItemName: undefined, + setSelectedItemName: noop, +}); + +export const useSelectionContext = (): ISelectionContext => useContext(SelectionContext); + +interface SelectionContextProviderProps { + children: React.JSX.Element; +} + +export function SelectionContextProvider(props: SelectionContextProviderProps): React.ReactElement { + const [selectedItemName, setSelectedItemName] = useState(undefined); + + return ( + + {props.children} + + ); +} diff --git a/frontend/src/routing/ModulesRegistry.ts b/frontend/src/routing/ModulesRegistry.ts index 6981a758..f5297e99 100644 --- a/frontend/src/routing/ModulesRegistry.ts +++ b/frontend/src/routing/ModulesRegistry.ts @@ -7,16 +7,20 @@ import Project from "../modules/Project"; import { ProjectIcon } from "../ui-lib/Icons/ProjectIcon"; import Nodes from "../modules/Nodes"; import { ExperimentsIcon } from "../ui-lib/Icons/ExperimentsIcon"; +import CalibrationGraph from "../modules/CalibrationGraph"; +import { CalibrationIcon } from "../ui-lib/Icons/CalibrationIcon"; const DATA_KEY: ModuleKey = "data"; const NODES_KEY: ModuleKey = "nodes"; const PROJECT_TAB: ModuleKey = "project"; +const CALIBRATION: ModuleKey = "calibration"; export type ModuleKey = | "components" | "notebook" | "dashboard" | "project" + | "calibration" | "data" | "nodes" | "experiments" @@ -73,6 +77,16 @@ export const ModulesRegistry: Array = [ dataCy: cyKeys.DATA_TAB, }, }, + { + keyId: CALIBRATION, + path: "calibration", + Component: CalibrationGraph, + menuItem: { + title: "Calibration", + icon: CalibrationIcon, + dataCy: cyKeys.CALIBRATION_TAB, + }, + }, ]; const modulesMap: { [key: string]: Module } = {}; diff --git a/frontend/src/ui-lib/Icons/CalibrationIcon.tsx b/frontend/src/ui-lib/Icons/CalibrationIcon.tsx new file mode 100644 index 00000000..7a513566 --- /dev/null +++ b/frontend/src/ui-lib/Icons/CalibrationIcon.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { IconProps } from "../../common/interfaces/IconProps"; + +export const CalibrationIcon: React.FunctionComponent = ({ width = 24, height = 24 }) => ( + + + + + +); diff --git a/frontend/src/ui-lib/Icons/PlayIcon.tsx b/frontend/src/ui-lib/Icons/PlayIcon.tsx new file mode 100644 index 00000000..2c0766fb --- /dev/null +++ b/frontend/src/ui-lib/Icons/PlayIcon.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { IconProps } from "../../common/interfaces/IconProps"; + +export const PlayIcon: React.FunctionComponent = ({ width = 24, height = 24 }) => ( + + + + + +); diff --git a/frontend/src/utils/api/apiRoutes.ts b/frontend/src/utils/api/apiRoutes.ts index 3479480a..3869c05e 100644 --- a/frontend/src/utils/api/apiRoutes.ts +++ b/frontend/src/utils/api/apiRoutes.ts @@ -18,5 +18,10 @@ export const ALL_PROJECTS = () => "api/projects/list"; export const ACTIVE_PROJECT = () => "api/projects/active"; export const IS_NODE_RUNNING = () => "execution/is_running"; export const ALL_NODES = () => "execution/get_nodes"; -export const SUBMIT_NODE_RUN = () => "execution/submit"; +export const GET_NODE = () => "execution/get_node"; +export const ALL_GRAPHS = () => "execution/get_graphs"; +export const GET_GRAPH = () => "execution/get_graph"; +export const GET_WORKFLOW_GRAPH = () => "execution/get_graph/cytoscape"; +export const SUBMIT_NODE_RUN = () => "execution/submit/node"; +export const SUBMIT_WORKFLOW_RUN = () => "execution/submit/workflow"; export const GET_LAST_RUN = () => "execution/last_run/"; diff --git a/frontend/src/utils/cyKeys.ts b/frontend/src/utils/cyKeys.ts index 0ad1b2d2..09c61612 100644 --- a/frontend/src/utils/cyKeys.ts +++ b/frontend/src/utils/cyKeys.ts @@ -6,6 +6,7 @@ const cyKeys = { DASHBOARD_TAB: "dashboard-tab", PROJECT_TAB: "project-tab", DATA_TAB: "data-tab", + CALIBRATION_TAB: "calibration-tab", NODES_TAB: "nodes-tab", NOTEBOOK_TAB: "notebook-tab", DOCS_TAB: "docs-tab",