diff --git a/frontend/src/common/interfaces/Api.ts b/frontend/src/common/interfaces/Api.ts index c32cf5f4..2f15748b 100644 --- a/frontend/src/common/interfaces/Api.ts +++ b/frontend/src/common/interfaces/Api.ts @@ -1,5 +1,7 @@ +import { NodeStatusErrorWithDetails } from "../../modules/Nodes/context/NodesContext"; + export type Res

> = { isOk: boolean; - error?: string | { detail: string }; + error?: string | { detail: string } | NodeStatusErrorWithDetails; result?: P; }; diff --git a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.module.scss b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.module.scss index cfe292a0..536e4dc6 100644 --- a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.module.scss +++ b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.module.scss @@ -16,20 +16,21 @@ .row { display: flex; justify-content: space-between; - flex: 1; flex-direction: row; padding-bottom: 10px; + max-width: 100%; } .dot { - height: 7px; - width: 7px; + min-height: 7px; + min-width: 7px; background-color: #bbb; border-radius: 50%; display: inline-block; } -.titleOrName { +.titleOrNameWrapper { + max-width: calc(100% - 127px); display: flex; flex: 1; font-size: 18px; @@ -38,6 +39,11 @@ gap: 10px; } +.titleOrName { + display: block; + max-width: 100%; +} + .description { align-content: center; font-size: 12px; diff --git a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx index fe2ed628..f0eefdb8 100644 --- a/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx +++ b/frontend/src/modules/Nodes/components/NodeElement/NodeElement.tsx @@ -3,7 +3,7 @@ import styles from "./NodeElement.module.scss"; import BlueButton from "../../../../ui-lib/components/Button/BlueButton"; import InputField from "../../../../DEPRECATED_components/common/Input/InputField"; import { Checkbox, CircularProgress } from "@mui/material"; -import { useNodesContext } from "../../context/NodesContext"; +import { NodeStatusErrorWithDetails, useNodesContext } from "../../context/NodesContext"; import { classNames } from "../../../../utils/classnames"; import { NodesApi } from "../../api/NodesAPI"; import { InputParameter, Parameters, SingleParameter } from "../../../common/Parameters"; @@ -78,11 +78,33 @@ export const NodeElement: React.FC<{ nodeKey: string; node: NodeDTO }> = ({ node return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; } - const handleClick = () => { + const handleClick = async () => { setIsNodeRunning(true); setRunningNode(node); - setRunningNodeInfo({ timestampOfRun: formatDate(new Date()), status: "running" }); - NodesApi.submitNodeParameters(node.name, transformInputParameters(node.parameters as InputParameter)); + const result = await NodesApi.submitNodeParameters(node.name, transformInputParameters(node.parameters as InputParameter)); + if (result.isOk) { + console.log("result", result); + setRunningNodeInfo({ timestampOfRun: formatDate(new Date()), status: "running" }); + } else { + const errorWithDetails = result.error as NodeStatusErrorWithDetails; + setRunningNodeInfo({ + timestampOfRun: formatDate(new Date()), + status: "error", + error: { + error_class: errorWithDetails.detail[0].type, + traceback: undefined, + message: errorWithDetails.detail[0].msg, + }, + }); + } + }; + + const insertSpaces = (str: string, interval = 40) => { + let result = ""; + for (let i = 0; i < str.length; i += interval) { + result += str.slice(i, i + interval) + " "; + } + return result.trim(); }; return ( @@ -93,9 +115,9 @@ export const NodeElement: React.FC<{ nodeKey: string; node: NodeDTO }> = ({ node }} >

-
+
- {node.title ?? node.name} +
{insertSpaces(node.title ?? node.name)}
{node.description}
diff --git a/frontend/src/modules/Nodes/components/RunningJob/RunningJob.module.scss b/frontend/src/modules/Nodes/components/RunningJob/RunningJob.module.scss index 619bcbe3..9bf48d45 100644 --- a/frontend/src/modules/Nodes/components/RunningJob/RunningJob.module.scss +++ b/frontend/src/modules/Nodes/components/RunningJob/RunningJob.module.scss @@ -12,6 +12,8 @@ } .title { + display: flex; + align-items: center; gap: 10px; font-size: 20px; vertical-align: middle; @@ -26,6 +28,18 @@ display: inline-block; } +.stopButtonWrapper { + display: flex; + flex: 1; + justify-content: flex-end; + align-items: center; + padding-right: 10px; +} + +.stopButton { + background-color: #e02525; +} + .infoWrapper { overflow: scroll; display: flex; @@ -96,6 +110,27 @@ font-size: 12px; } +.stateUpdateValueTextWrapper { + padding-top: 5px; + align-items: center; + display: flex; +} + +.editIconWrapper { + padding-left: 5px; +} + +.newValueOfState { + padding-left: 5px; + + input { + font-size: 12px; + padding: 0 !important; + height: 20px !important; + } + +} + .nodeStatusErrorWrapper { overflow: scroll; padding-left: 52px; diff --git a/frontend/src/modules/Nodes/components/RunningJob/RunningJob.tsx b/frontend/src/modules/Nodes/components/RunningJob/RunningJob.tsx index 46805cc4..346ea174 100644 --- a/frontend/src/modules/Nodes/components/RunningJob/RunningJob.tsx +++ b/frontend/src/modules/Nodes/components/RunningJob/RunningJob.tsx @@ -6,179 +6,234 @@ import { UpArrowIcon } from "../../../../ui-lib/Icons/UpArrowIcon"; import { SnapshotsApi } from "../../../Snapshots/api/SnapshotsApi"; import { CheckMarkIcon } from "../../../../ui-lib/Icons/CheckMarkIcon"; import { RightArrowIcon } from "../../../../ui-lib/Icons/RightArrowIcon"; +import { EditIcon } from "../../../../ui-lib/Icons/EditIcon"; +import InputField from "../../../../DEPRECATED_components/common/Input/InputField"; +import BlueButton from "../../../../ui-lib/components/Button/BlueButton"; interface StateUpdateComponentProps { - key: string; - stateUpdateObject: StateUpdateObject; - runningNodeInfo?: RunningNodeInfo; + key: string; + stateUpdateObject: StateUpdateObject; + runningNodeInfo?: RunningNodeInfo; } const StateUpdateComponent: React.FC = (props) => { - const { key, stateUpdateObject, runningNodeInfo } = props; - const [runningUpdate, setRunningUpdate] = React.useState(false); - const [parameterUpdated, setParameterUpdated] = useState(false); + const { key, stateUpdateObject, runningNodeInfo } = props; + const [runningUpdate, setRunningUpdate] = React.useState(false); + const [parameterUpdated, setParameterUpdated] = useState(false); + const [editMode, setEditMode] = useState(false); + const [customValue, setCustomValue] = useState(undefined); - const renderSuggestedValue = (value: string | number | number[]) => { - if (Array.isArray(value)) { - return "[" + value.toString() + "]"; - } - return value.toString(); - }; + const renderSuggestedValue = (value: string | number | number[]) => { + if (Array.isArray(value)) { + return "[" + value.toString() + "]"; + } + return value.toString(); + }; - return ( -
-
- {!runningUpdate && !parameterUpdated && ( -
{ - if ( - runningNodeInfo && - runningNodeInfo.idx && - stateUpdateObject && - ("val" in stateUpdateObject || "new" in stateUpdateObject) - ) { - setRunningUpdate(true); - const response = await SnapshotsApi.updateState( - runningNodeInfo?.idx, - key, - (stateUpdateObject.val ? stateUpdateObject.val : stateUpdateObject.new!).toString() - ); - setRunningUpdate(false); - if (response.isOk) { - setParameterUpdated(response.result!); - } else { - setParameterUpdated(response.result!); //TODO Check this - } - } - }} - > - -
- )} -
- {runningUpdate && !parameterUpdated && } - {!runningUpdate && parameterUpdated && } -
-
-
-
{stateUpdateObject?.key ? stateUpdateObject?.key.toString() : key.toString()}
-
- {stateUpdateObject && ( + return ( +
- {stateUpdateObject.old}   - -   {renderSuggestedValue(stateUpdateObject.val ?? stateUpdateObject.new ?? "")} + {!runningUpdate && !parameterUpdated && ( +
{ + if ( + runningNodeInfo && + runningNodeInfo.idx && + stateUpdateObject && + ("val" in stateUpdateObject || "new" in stateUpdateObject) + ) { + setRunningUpdate(true); + const stateUpdateValue = customValue ? customValue : stateUpdateObject.val ?? stateUpdateObject.new!; + const response = await SnapshotsApi.updateState(runningNodeInfo?.idx, key, stateUpdateValue.toString()); + if (response.isOk) { + setParameterUpdated(response.result!); + } else { + setParameterUpdated(response.result!); //TODO Check this + } + setRunningUpdate(false); + } + }} + > + +
+ )} +
+ {runningUpdate && !parameterUpdated && } + {!runningUpdate && parameterUpdated && } +
+
+
+
{stateUpdateObject?.key ? stateUpdateObject?.key.toString() : key.toString()}
+
+ {stateUpdateObject && ( +
+ {stateUpdateObject.old}   + +   {renderSuggestedValue(stateUpdateObject.val ?? stateUpdateObject.new ?? "")} +
{ + setEditMode(true); + setCustomValue(stateUpdateObject.val ?? stateUpdateObject.new); + }} + > + {!editMode && } +
+ {editMode && ( + { + setCustomValue(val); + }} + /> + )} +
+ )} +
- )}
-
-
- ); + ); }; const GetStateUpdates: React.FC<{ - runningNodeInfo: RunningNodeInfo | undefined; + runningNodeInfo: RunningNodeInfo | undefined; }> = (props) => { - const { runningNodeInfo } = props; - return ( - <> - {runningNodeInfo?.state_updates &&
State updates:
} - {runningNodeInfo?.state_updates && ( -
- {Object.entries(runningNodeInfo?.state_updates ?? {}).map(([key, stateUpdateObject]) => - StateUpdateComponent({ - key, - stateUpdateObject, - runningNodeInfo, - } as StateUpdateComponentProps) - )} -
- )} - - ); + const { runningNodeInfo } = props; + return ( + <> + {runningNodeInfo?.state_updates &&
State updates:
} + {runningNodeInfo?.state_updates && ( +
+ {Object.entries(runningNodeInfo?.state_updates ?? {}).map(([key, stateUpdateObject]) => + StateUpdateComponent({ + key, + stateUpdateObject, + runningNodeInfo, + } as StateUpdateComponentProps) + )} +
+ )} + + ); }; const NodeStatusErrorWrapper: React.FC<{ - runningNodeInfo: RunningNodeInfo | undefined; + runningNodeInfo: RunningNodeInfo | undefined; }> = (props) => { - const { runningNodeInfo } = props; - return ( - <> - {runningNodeInfo?.error &&
Error traceback:
} -
- {(runningNodeInfo?.error?.traceback ?? []).map((row, index) => ( -
- {row} -
- ))} -
- - ); + const { runningNodeInfo } = props; + let errorMessage = runningNodeInfo?.error?.error_class; + if (errorMessage) { + errorMessage += ": "; + } + if (runningNodeInfo?.error?.message) { + errorMessage += runningNodeInfo?.error?.message; + } + + return ( + <> + {/*{runningNodeInfo?.error &&
Error traceback:
}*/} +
+ {runningNodeInfo?.error?.error_class &&
Error occurred:
} +
{errorMessage}
+ {runningNodeInfo?.error?.traceback?.length && runningNodeInfo?.error?.traceback?.length > 0 &&
Error traceback:
} + {(runningNodeInfo?.error?.traceback ?? []).map((row, index) => ( +
+ {row} +
+ ))} +
+ + ); }; export const RunningJob: React.FC = () => { - const { runningNode, runningNodeInfo } = useNodesContext(); + const { runningNode, runningNodeInfo, isNodeRunning, setIsNodeRunning } = useNodesContext(); - const getRunningJobInfo = () => { - return ( -
- {runningNodeInfo?.lastRunNodeName && ( -
Last run node:  {runningNodeInfo?.lastRunNodeName}
- )} - {runningNodeInfo?.timestampOfRun && ( -
Run start:  {runningNodeInfo?.timestampOfRun}
- )} - {runningNodeInfo?.runDuration && ( -
Run duration:  {runningNodeInfo?.runDuration} seconds
- )} - {runningNodeInfo?.status &&
Status:  {runningNodeInfo?.status}
} - {runningNodeInfo?.idx &&
idx:  {runningNodeInfo?.idx}
} -
- ); - }; + const getRunningJobInfo = () => { + return ( +
+ {runningNodeInfo?.lastRunNodeName && ( +
Last run node:  {runningNodeInfo?.lastRunNodeName}
+ )} + {runningNodeInfo?.timestampOfRun && ( +
Run start:  {runningNodeInfo?.timestampOfRun}
+ )} + {runningNodeInfo?.runDuration && ( +
Run duration:  {runningNodeInfo?.runDuration} seconds
+ )} + {runningNodeInfo?.status &&
Status:  {runningNodeInfo?.status}
} + {runningNodeInfo?.idx &&
idx:  {runningNodeInfo?.idx}
} +
+ ); + }; + + const getRunningJobParameters = () => { + return ( + <> + {Object.entries(runningNode?.parameters ?? {}).length > 0 && ( +
+
+ {/*
setExpanded(!expanded)}>*/} + {/* */} + {/*
*/} + Parameters: +
+
+ { + // expanded && + Object.entries(runningNode?.parameters ?? {}).map(([key, parameter]) => ( +
+
{parameter.title}:
+
{parameter.default?.toString()}
+
+ )) + } +
+
+ )} + + ); + }; + const insertSpaces = (str: string, interval = 40) => { + let result = ""; + for (let i = 0; i < str.length; i += interval) { + result += str.slice(i, i + interval) + " "; + } + return result.trim(); + }; + + const handleStopClick = () => { + SnapshotsApi.stopNodeRunning().then((res) => { + if (res.isOk) { + setIsNodeRunning(!res.result); + } + }); + }; - const getRunningJobParameters = () => { return ( - <> - {Object.entries(runningNode?.parameters ?? {}).length > 0 && ( -
-
- {/*
setExpanded(!expanded)}>*/} - {/* */} - {/*
*/} - Parameters: -
-
- { - // expanded && - Object.entries(runningNode?.parameters ?? {}).map(([key, parameter]) => ( -
-
{parameter.title}:
-
{parameter.default?.toString()}
-
- )) - } +
+
+
+
+ Running job {runningNode?.name ? ":" : ""}  {runningNode?.name ? insertSpaces(runningNode?.name) : ""} +
+ {isNodeRunning && ( +
+ + Stop + +
+ )}
-
- )} - - ); - }; - - return ( -
-
-
- Running job {runningNode?.name ? ":" : ""}  {runningNode?.name ?? ""} -
- {runningNodeInfo && ( -
- {getRunningJobInfo()} - {getRunningJobParameters()} + {runningNodeInfo && ( +
+ {getRunningJobInfo()} + {getRunningJobParameters()} +
+ )} + +
- )} - - -
- ); + ); }; diff --git a/frontend/src/modules/Nodes/context/NodesContext.tsx b/frontend/src/modules/Nodes/context/NodesContext.tsx index 98da1440..166b0903 100644 --- a/frontend/src/modules/Nodes/context/NodesContext.tsx +++ b/frontend/src/modules/Nodes/context/NodesContext.tsx @@ -60,13 +60,17 @@ interface NodesContextProviderProps { children: React.JSX.Element; } -interface NodeStatusError { +export interface NodeStatusError { error_class: string; message: string; - traceback: string[]; + traceback?: string[]; } -interface NodeStatusResponseType { +export interface NodeStatusErrorWithDetails { + detail: { msg: string; type: string }[]; +} + +export interface NodeStatusResponseType { idx: number; status: string; error?: NodeStatusError; @@ -154,6 +158,12 @@ export function NodesContextProvider(props: NodesContextProviderProps): React.Re useEffect(() => { if (!isNodeRunning) { fetchNodeResults(); + if (runningNodeInfo?.status === "running") { + setRunningNodeInfo({ + ...runningNodeInfo, + status: "finished", + }); + } } }, [isNodeRunning]); diff --git a/frontend/src/modules/Snapshots/api/SnapshotsApi.tsx b/frontend/src/modules/Snapshots/api/SnapshotsApi.tsx index 6ec56c99..b1cd75aa 100644 --- a/frontend/src/modules/Snapshots/api/SnapshotsApi.tsx +++ b/frontend/src/modules/Snapshots/api/SnapshotsApi.tsx @@ -1,61 +1,70 @@ -import Api, {BASIC_HEADERS} from "../../../utils/api"; -import {Res} from "../../../common/interfaces/Api"; +import Api, { BASIC_HEADERS } from "../../../utils/api"; +import { Res } from "../../../common/interfaces/Api"; import { ALL_SNAPSHOTS, ONE_SNAPSHOT, SNAPSHOT_DIFF, SNAPSHOT_RESULT, - UPDATE_SNAPSHOT + STOP_NODE_RUNNING, + UPDATE_SNAPSHOT, } from "../../../utils/api/apiRoutes"; -import {API_METHODS} from "../../../common/enums/Api"; -import {SnapshotDTO} from "../SnapshotDTO"; +import { API_METHODS } from "../../../common/enums/Api"; +import { SnapshotDTO } from "../SnapshotDTO"; export interface SnapshotResult { - items: SnapshotDTO[]; - per_page: number; - page: number; - total_items: number; - total_pages: number; + items: SnapshotDTO[]; + per_page: number; + page: number; + total_items: number; + total_pages: number; } export class SnapshotsApi extends Api { - constructor() { - super(); - } - - static api(path: string): string { - return this.address + path; - } - - static fetchAllSnapshots(pageNumber: number): Promise> { - return this._fetch(this.api(ALL_SNAPSHOTS({pageNumber})), API_METHODS.GET, { - headers: BASIC_HEADERS, - }); - } - - static fetchSnapshot(id: string): Promise> { - return this._fetch(this.api(ONE_SNAPSHOT(id)), API_METHODS.GET, { - headers: BASIC_HEADERS, - }); - } - - static fetchSnapshotResult(id: string): Promise> { - return this._fetch(this.api(SNAPSHOT_RESULT(id)), API_METHODS.GET, { - headers: BASIC_HEADERS, - }); - } - - static fetchSnapshotUpdate(currentId: string, newId: string): Promise> { - return this._fetch(this.api(SNAPSHOT_DIFF(currentId, newId)), API_METHODS.GET, { - headers: BASIC_HEADERS, - }); - } - - static updateState(snapshotId: string, data_path: string, value: string): Promise> { - return this._fetch(this.api(UPDATE_SNAPSHOT(snapshotId)), API_METHODS.POST, { - headers: BASIC_HEADERS, - body: JSON.stringify({data_path, value}), - queryParams: {data_path, value}, - }); - } + constructor() { + super(); + } + + static api(path: string): string { + return this.address + path; + } + + static fetchAllSnapshots(pageNumber: number): Promise> { + return this._fetch(this.api(ALL_SNAPSHOTS({ pageNumber })), API_METHODS.GET, { + headers: BASIC_HEADERS, + }); + } + + static fetchSnapshot(id: string): Promise> { + return this._fetch(this.api(ONE_SNAPSHOT(id)), API_METHODS.GET, { + headers: BASIC_HEADERS, + }); + } + + static fetchSnapshotResult(id: string): Promise> { + return this._fetch(this.api(SNAPSHOT_RESULT(id)), API_METHODS.GET, { + headers: BASIC_HEADERS, + }); + } + + static fetchSnapshotUpdate(currentId: string, newId: string): Promise> { + return this._fetch(this.api(SNAPSHOT_DIFF(currentId, newId)), API_METHODS.GET, { + headers: BASIC_HEADERS, + }); + } + + static updateState(snapshotId: string, data_path: string, value: string): Promise> { + return this._fetch(this.api(UPDATE_SNAPSHOT(snapshotId)), API_METHODS.POST, { + headers: BASIC_HEADERS, + body: JSON.stringify({ data_path, value }), + queryParams: { data_path, value }, + }); + } + + static stopNodeRunning(): Promise> { + return this._fetch(this.api(STOP_NODE_RUNNING()), API_METHODS.POST, { + headers: BASIC_HEADERS, + // body: JSON.stringify({ data_path, value }), + // queryParams: { data_path, value }, + }); + } } diff --git a/frontend/src/ui-lib/Icons/EditIcon.tsx b/frontend/src/ui-lib/Icons/EditIcon.tsx index 9157f6b1..02680d88 100644 --- a/frontend/src/ui-lib/Icons/EditIcon.tsx +++ b/frontend/src/ui-lib/Icons/EditIcon.tsx @@ -1,17 +1,28 @@ -import { ACTIVE_TEXT } from "../../utils/colors"; import { IconProps } from "../../common/interfaces/IconProps"; import React from "react"; -export const EditIcon: React.FunctionComponent = ({ width = 24, height = 24, color = ACTIVE_TEXT }) => ( - - - - - +export const EditIcon: React.FunctionComponent = ({ width = 13, height = 13 }) => ( + + + + + + + + ); diff --git a/frontend/src/ui-lib/Icons/EditIconOld.tsx b/frontend/src/ui-lib/Icons/EditIconOld.tsx new file mode 100644 index 00000000..3df7e38e --- /dev/null +++ b/frontend/src/ui-lib/Icons/EditIconOld.tsx @@ -0,0 +1,17 @@ +import { ACTIVE_TEXT } from "../../utils/colors"; +import { IconProps } from "../../common/interfaces/IconProps"; +import React from "react"; + +export const EditIconOld: React.FunctionComponent = ({ width = 24, height = 24, color = ACTIVE_TEXT }) => ( + + + + + + + + +); diff --git a/frontend/src/ui-lib/Icons/RightArrowIcon.tsx b/frontend/src/ui-lib/Icons/RightArrowIcon.tsx index 55d47a6a..e456aa3b 100644 --- a/frontend/src/ui-lib/Icons/RightArrowIcon.tsx +++ b/frontend/src/ui-lib/Icons/RightArrowIcon.tsx @@ -2,7 +2,7 @@ import React from "react"; import { IconProps } from "../../common/interfaces/IconProps"; export const RightArrowIcon: React.FunctionComponent = ({ width = 16, height = 16 }) => ( - + "api/projects/active"; export const IS_NODE_RUNNING = () => "execution/is_running"; +export const STOP_NODE_RUNNING = () => "api/execution/stop"; export const ALL_NODES = () => "execution/get_nodes"; export const GET_NODE = () => "execution/get_node"; export const ALL_GRAPHS = () => "execution/get_graphs";