From 85d3ae06baf7abbbe12e1b36eb1a8507671ed908 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:16:16 +0100 Subject: [PATCH] feat(ui-study): freeze study when blocking tasks running --- .../App/Singlestudy/FreezeStudy.tsx | 180 ++++++++++++++++++ .../TimeSeriesManagement/index.tsx | 15 +- .../src/components/App/Singlestudy/index.tsx | 3 + 3 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 webapp/src/components/App/Singlestudy/FreezeStudy.tsx diff --git a/webapp/src/components/App/Singlestudy/FreezeStudy.tsx b/webapp/src/components/App/Singlestudy/FreezeStudy.tsx new file mode 100644 index 0000000000..a1921898a9 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/FreezeStudy.tsx @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2024, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import { StudyMetadata } from "@/common/types"; +import { getTasks } from "@/services/api/tasks"; +import { TaskStatus, TaskType } from "@/services/api/tasks/constants"; +import type { TaskDTO, TTaskType } from "@/services/api/tasks/types"; +import { WsChannel, WsEventType } from "@/services/webSocket/constants"; +import { TaskEventPayload, WsEvent } from "@/services/webSocket/types"; +import { + addWsEventListener, + removeWsEventListener, + subscribeWsChannels, + unsubscribeWsChannels, +} from "@/services/webSocket/ws"; +import { + Backdrop, + Button, + List, + ListItem, + ListItemText, + Paper, +} from "@mui/material"; +import { useEffect, useState } from "react"; +import LinearProgressWithLabel from "@/components/common/LinearProgressWithLabel"; +import { useTranslation } from "react-i18next"; + +const BLOCKING_TASK_TYPES = [ + TaskType.UpgradeStudy, + TaskType.ThermalClusterSeriesGeneration, +] as const; + +function getChannel(id: TaskDTO["id"]) { + return WsChannel.Task + id; +} + +interface BlockingTask { + id: TaskDTO["id"]; + type: TTaskType; + progress: number; + failed?: boolean; +} + +interface Props { + studyId: StudyMetadata["id"]; +} + +function FreezeStudy({ studyId }: Props) { + const [blockingTasks, setBlockingTasks] = useState([]); + const { t } = useTranslation(); + const hasLoadingTask = !!blockingTasks.find( + (task) => task.progress !== 100 && !task.failed, + ); + + useEffect(() => { + let ignore = false; // Prevent race condition + + getTasks({ + studyId, + type: [TaskType.UpgradeStudy, TaskType.ThermalClusterSeriesGeneration], + status: [TaskStatus.Pending, TaskStatus.Running], + }).then((tasks) => { + if (!ignore) { + setBlockingTasks( + tasks.map((task) => ({ + id: task.id, + type: task.type!, + progress: -1, + })), + ); + + subscribeWsChannels(tasks.map(({ id }) => getChannel(id))); + } + }); + + const listener = (event: WsEvent) => { + console.log(event.type); + switch (event.type) { + case WsEventType.TaskAdded: { + const { id, type } = event.payload; + + if (BLOCKING_TASK_TYPES.includes(type)) { + setBlockingTasks((tasks) => [ + ...tasks, + { + ...event.payload, + progress: -1, + }, + ]); + + subscribeWsChannels(getChannel(id)); + } + break; + } + case WsEventType.TsGenerationProgress: { + setBlockingTasks((tasks) => + tasks.map((task) => + task.id === event.payload.task_id + ? { ...task, progress: event.payload.progress } + : task, + ), + ); + break; + } + case WsEventType.TaskFailed: { + const { id } = event.payload; + setBlockingTasks((tasks) => + tasks.map((task) => + task.id === id ? { ...task, failed: true } : task, + ), + ); + unsubscribeWsChannels(getChannel(id)); + break; + } + case WsEventType.TaskCompleted: { + const { id } = event.payload; + setBlockingTasks((tasks) => + tasks.map((task) => + task.id === id ? { ...task, progress: 100 } : task, + ), + ); + unsubscribeWsChannels(getChannel(id)); + break; + } + } + }; + + addWsEventListener(listener); + + return () => { + ignore = true; + removeWsEventListener(listener); + unsubscribeWsChannels(); + }; + }, [studyId]); + + return ( + 0} sx={{ position: "absolute" }}> + + + {blockingTasks.map(({ id, type, progress, failed }) => ( + + + } + secondary={t(`tasks.type.${type}`)} + /> + + ))} + + {!hasLoadingTask && ( + + )} + + + ); +} + +export default FreezeStudy; diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx index a6db1b063f..7e4acb4654 100644 --- a/webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx @@ -26,10 +26,12 @@ import { useTranslation } from "react-i18next"; import usePromiseHandler from "../../../../../../hooks/usePromiseHandler"; import { generateTimeSeries } from "../../../../../../services/api/studies/timeseries"; import BuildIcon from "@mui/icons-material/Build"; +import { useState } from "react"; function TimeSeriesManagement() { const { study } = useOutletContext<{ study: StudyMetadata }>(); const { t } = useTranslation(); + const [generationInProgress, setGenerationInProgress] = useState(false); const handleGenerateTs = usePromiseHandler({ fn: generateTimeSeries, @@ -45,9 +47,13 @@ function TimeSeriesManagement() { return setTimeSeriesFormFields(study.id, data.dirtyValues); }; - const handleSubmitSuccessful = () => { + const handleSubmitSuccessful = async () => { + setGenerationInProgress(true); + // The WebSocket will trigger an event after the fulfillment of the promise - handleGenerateTs({ studyId: study.id }); + await handleGenerateTs({ studyId: study.id }); + + setGenerationInProgress(false); }; //////////////////////////////////////////////////////////////// @@ -57,7 +63,10 @@ function TimeSeriesManagement() { return (
getTimeSeriesFormFields(study.id) }} + config={{ + defaultValues: () => getTimeSeriesFormFields(study.id), + disabled: generationInProgress, + }} onSubmit={handleSubmit} onSubmitSuccessful={handleSubmitSuccessful} submitButtonText={t("study.configuration.tsManagement.generateTs")} diff --git a/webapp/src/components/App/Singlestudy/index.tsx b/webapp/src/components/App/Singlestudy/index.tsx index bdf32699b6..6823d6db54 100644 --- a/webapp/src/components/App/Singlestudy/index.tsx +++ b/webapp/src/components/App/Singlestudy/index.tsx @@ -37,6 +37,7 @@ import CommandDrawer from "./Commands"; import { addWsEventListener } from "../../../services/webSocket/ws"; import useAppDispatch from "../../../redux/hooks/useAppDispatch"; import SimpleLoader from "../../common/loaders/SimpleLoader"; +import FreezeStudy from "./FreezeStudy"; import { WsEvent } from "@/services/webSocket/types"; import { WsEventType } from "@/services/webSocket/constants"; @@ -186,12 +187,14 @@ function SingleStudy(props: Props) { alignItems="center" boxSizing="border-box" overflow="hidden" + position="relative" > {isExplorer === true ? ( ) : ( )} + {openCommands && studyId && (