From dd10840231496d95f0ca12b6cb41fee79e7f14c7 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 1 Mar 2024 15:29:32 +0100 Subject: [PATCH 01/11] fix(ui-common): disable `strechH` for all matrices --- webapp/src/components/common/EditableMatrix/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/src/components/common/EditableMatrix/index.tsx b/webapp/src/components/common/EditableMatrix/index.tsx index 87aa8441f2..15f9984aa0 100644 --- a/webapp/src/components/common/EditableMatrix/index.tsx +++ b/webapp/src/components/common/EditableMatrix/index.tsx @@ -178,7 +178,6 @@ function EditableMatrix(props: PropTypes) { data={grid} width="100%" height="100%" - stretchH={stretch ? "all" : "none"} className="editableMatrix" colHeaders rowHeaderWidth={matrixRowNames ? 150 : undefined} From 2c64d1054d589ad130fc1fa8430f569528cb3b1d Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 1 Mar 2024 15:36:56 +0100 Subject: [PATCH 02/11] refactor(ui-common): remove unused `stretch` prop --- .../explore/Modelization/Areas/Hydro/HydroMatrix.tsx | 1 - .../App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts | 3 --- .../App/Singlestudy/explore/Results/ResultDetails/index.tsx | 1 - webapp/src/components/common/EditableMatrix/index.tsx | 2 -- webapp/src/components/common/MatrixInput/index.tsx | 3 --- 5 files changed, 10 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index 12d8a773d3..2a8796074c 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -32,7 +32,6 @@ function HydroMatrix({ type }: Props) { fetchFn={hydroMatrix.fetchFn} disableEdit={hydroMatrix.disableEdit} enablePercentDisplay={hydroMatrix.enablePercentDisplay} - stretch={hydroMatrix.stretch} /> ); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index 008d271eb2..8fc143d280 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -36,7 +36,6 @@ export interface HydroMatrixProps { fetchFn?: fetchMatrixFn; disableEdit?: boolean; enablePercentDisplay?: boolean; - stretch?: boolean; // TODO: Remove this once the `EditableMatrix` component is refactored } type Matrices = Record; @@ -121,7 +120,6 @@ export const MATRICES: Matrices = { "Pumping Max Energy (Hours at Pmax)", ], stats: MatrixStats.NOCOL, - stretch: false, }, [HydroMatrixType.ReservoirLevels]: { title: "Reservoir Levels", @@ -177,7 +175,6 @@ export const MATRICES: Matrices = { "December", ], stats: MatrixStats.NOCOL, - stretch: false, }, [HydroMatrixType.Allocation]: { title: "Allocation", diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index e61c2ac3d2..ab74bd3a83 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -380,7 +380,6 @@ function ResultDetails() { matrix={matrix} matrixTime={false} rowNames={dateTimeFromIndex} - stretch={false} readOnly /> ) diff --git a/webapp/src/components/common/EditableMatrix/index.tsx b/webapp/src/components/common/EditableMatrix/index.tsx index 15f9984aa0..b53b726985 100644 --- a/webapp/src/components/common/EditableMatrix/index.tsx +++ b/webapp/src/components/common/EditableMatrix/index.tsx @@ -28,7 +28,6 @@ interface PropTypes { rowNames?: string[]; computStats?: MatrixStats; isPercentDisplayEnabled?: boolean; - stretch?: boolean; } type CellType = Array; @@ -56,7 +55,6 @@ function EditableMatrix(props: PropTypes) { rowNames, computStats, isPercentDisplayEnabled = false, - stretch = true, } = props; const { data = [], columns = [], index = [] } = matrix; const prependIndex = index.length > 0 && matrixTime; diff --git a/webapp/src/components/common/MatrixInput/index.tsx b/webapp/src/components/common/MatrixInput/index.tsx index 7807cd94c4..0d43522296 100644 --- a/webapp/src/components/common/MatrixInput/index.tsx +++ b/webapp/src/components/common/MatrixInput/index.tsx @@ -38,7 +38,6 @@ interface Props { fetchFn?: fetchMatrixFn; disableEdit?: boolean; enablePercentDisplay?: boolean; - stretch?: boolean; } function MatrixInput({ @@ -51,7 +50,6 @@ function MatrixInput({ fetchFn, disableEdit, enablePercentDisplay, - stretch, }: Props) { const { enqueueSnackbar } = useSnackbar(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); @@ -213,7 +211,6 @@ function MatrixInput({ onUpdate={handleUpdate} computStats={computStats} isPercentDisplayEnabled={enablePercentDisplay} - stretch={stretch} /> ) : ( !isLoading && ( From 9fbb13ea09dec4b41d797d33e528f222df911536 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 1 Mar 2024 15:38:06 +0100 Subject: [PATCH 03/11] feat(ui-common): reduce index col width for matrices --- webapp/src/components/common/EditableMatrix/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/common/EditableMatrix/index.tsx b/webapp/src/components/common/EditableMatrix/index.tsx index b53b726985..3e17ed5aaa 100644 --- a/webapp/src/components/common/EditableMatrix/index.tsx +++ b/webapp/src/components/common/EditableMatrix/index.tsx @@ -178,7 +178,7 @@ function EditableMatrix(props: PropTypes) { height="100%" className="editableMatrix" colHeaders - rowHeaderWidth={matrixRowNames ? 150 : undefined} + rowHeaderWidth={rowNames ? 150 : 50} afterChange={(change, source) => onUpdate && handleSlice(change || [], source) } From c68f27dae10de3f3967fb325beb108088f01c661 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 1 Mar 2024 17:16:21 +0100 Subject: [PATCH 04/11] feat(ui-tasks): add launcher metrics --- webapp/public/locales/en/main.json | 4 +- webapp/public/locales/fr/main.json | 4 +- .../src/components/App/Tasks/JobTableView.tsx | 55 +++++++++++++------ webapp/src/services/api/study.ts | 10 ++-- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 81b9935f03..b088d7e0cd 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -196,7 +196,9 @@ "study.postProcessing": "Post processing", "study.timeLimit": "Time limit (h)", "study.nbCpu": "Number of cores", - "study.clusterLoad": "Cluster load", + "study.allocatedCpuRate": "CPU usage", + "study.clusterLoadRate": "Cluster load", + "study.nbQueuedJobs": "Pending jobs", "study.synthesis": "Synthesis", "study.level": "Level", "study.years": "Years", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 0f1f06ad45..0abb2c9dbc 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -196,7 +196,9 @@ "study.postProcessing": "Post-traitement", "study.timeLimit": "Limite de temps (h)", "study.nbCpu": "Nombre de coeurs", - "study.clusterLoad": "Charge du cluster", + "study.allocatedCpuRate": "Utilisation CPU", + "study.clusterLoadRate": "Charge cluster", + "study.nbQueuedJobs": "Tâches en attente", "study.synthesis": "Synthèse", "study.level": "Niveau", "study.years": "Années", diff --git a/webapp/src/components/App/Tasks/JobTableView.tsx b/webapp/src/components/App/Tasks/JobTableView.tsx index 6a2003507e..1381489764 100644 --- a/webapp/src/components/App/Tasks/JobTableView.tsx +++ b/webapp/src/components/App/Tasks/JobTableView.tsx @@ -19,6 +19,7 @@ import { Checkbox, FormControlLabel, Typography, + Skeleton, } from "@mui/material"; import { useTranslation } from "react-i18next"; import RefreshIcon from "@mui/icons-material/Refresh"; @@ -27,8 +28,9 @@ import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import { grey } from "@mui/material/colors"; import { TaskView, TaskType } from "../../../common/types"; import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; -import { getLauncherLoad } from "../../../services/api/study"; +import { getLauncherMetrics } from "../../../services/api/study"; import LinearProgressWithLabel from "../../common/LinearProgressWithLabel"; +import UsePromiseCond from "../../common/utils/UsePromiseCond"; interface PropType { content: TaskView[]; @@ -44,11 +46,13 @@ function JobTableView(props: PropType) { useState(false); const [currentContent, setCurrentContent] = useState(content); - const { data: load, reload: reloadLauncherLoad } = - usePromiseWithSnackbarError(() => getLauncherLoad(), { + const laucherMetrics = usePromiseWithSnackbarError( + () => getLauncherMetrics(), + { errorMessage: t("study.error.launchLoad"), deps: [], - }); + }, + ); const applyFilter = useCallback( (taskList: TaskView[]) => { @@ -109,21 +113,38 @@ function JobTableView(props: PropType) { > - {t("study.clusterLoad")} - {load && ( - - )} + ( + <> + {t("study.allocatedCpuRate")} + + {t("study.clusterLoadRate")} + + + {t("study.nbQueuedJobs")}: {data.nbQueuedJobs} + + + )} + ifPending={() => } + /> @@ -131,7 +152,7 @@ function JobTableView(props: PropType) { color="primary" onClick={() => { refresh(); - reloadLauncherLoad(); + laucherMetrics.reload(); }} variant="outlined" > diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 3b346e6d8d..9ee256d299 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -287,9 +287,11 @@ export const launchStudy = async ( return res.data; }; -interface LauncherLoadDTO { - slurm: number; - local: number; +interface LauncherMetrics { + allocatedCpuRate: number; + clusterLoadRate: number; + nbQueuedJobs: number; + status: string; } export const getLauncherVersions = async (): Promise => { @@ -302,7 +304,7 @@ export const getLauncherCores = async (): Promise> => { return res.data; }; -export const getLauncherLoad = async (): Promise => { +export const getLauncherMetrics = async (): Promise => { const res = await client.get("/v1/launcher/load"); return res.data; }; From 4201e88a316b8c678e2a3004dfd703eeef10c01d Mon Sep 17 00:00:00 2001 From: mabw-rte <41002227+mabw-rte@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:59:20 +0100 Subject: [PATCH 05/11] fix(api-study): check area duplicates on creation (#1964) --- antarest/core/exceptions.py | 8 ++++++++ antarest/study/business/area_management.py | 12 ++++++++++-- tests/integration/test_integration.py | 19 +++++++++++-------- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/antarest/core/exceptions.py b/antarest/core/exceptions.py index 9a2230c1d1..cada3a5f5d 100644 --- a/antarest/core/exceptions.py +++ b/antarest/core/exceptions.py @@ -253,6 +253,14 @@ def __init__(self, *area_ids: str) -> None: super().__init__(HTTPStatus.NOT_FOUND, msg) +class DuplicateAreaName(HTTPException): + """Exception raised when trying to create an area with an already existing name.""" + + def __init__(self, area_name: str) -> None: + msg = f"Area '{area_name}' already exists and could not be created" + super().__init__(HTTPStatus.CONFLICT, msg) + + class DistrictNotFound(HTTPException): def __init__(self, *district_ids: str) -> None: count = len(district_ids) diff --git a/antarest/study/business/area_management.py b/antarest/study/business/area_management.py index 369b5a96ad..544f18d8cf 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -5,7 +5,7 @@ from pydantic import BaseModel -from antarest.core.exceptions import LayerNotAllowedToBeDeleted, LayerNotFound +from antarest.core.exceptions import DuplicateAreaName, LayerNotAllowedToBeDeleted, LayerNotFound from antarest.study.business.utils import execute_or_add_commands from antarest.study.model import Patch, PatchArea, PatchCluster, RawStudy, Study from antarest.study.repository import StudyMetadataRepository @@ -318,12 +318,20 @@ def remove_layer(self, study: RawStudy, layer_id: str) -> None: def create_area(self, study: Study, area_creation_info: AreaCreationDTO) -> AreaInfoDTO: file_study = self.storage_service.get_storage(study).get_raw(study) + + # check if area already exists + area_id = transform_name_to_id(area_creation_info.name) + if area_id in set(file_study.config.areas): + raise DuplicateAreaName(area_creation_info.name) + + # Create area and apply changes in the study command = CreateArea( area_name=area_creation_info.name, command_context=self.storage_service.variant_study_service.command_factory.command_context, ) execute_or_add_commands(study, file_study, [command], self.storage_service) - area_id = transform_name_to_id(area_creation_info.name) + + # Update metadata patch = self.patch_service.get(study) patch.areas = patch.areas or {} patch.areas[area_id] = area_creation_info.metadata or PatchArea() diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 1dc1b385ae..c927e576af 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -9,7 +9,7 @@ from antarest.core.model import PublicMode from antarest.launcher.model import LauncherLoadDTO from antarest.study.business.adequacy_patch_management import PriceTakingOrder -from antarest.study.business.area_management import AreaType, LayerInfoDTO +from antarest.study.business.area_management import LayerInfoDTO from antarest.study.business.areas.properties_management import AdequacyPatchMode from antarest.study.business.areas.renewable_management import TimeSeriesInterpretation from antarest.study.business.general_management import Mode @@ -420,23 +420,26 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: headers=admin_headers, json={ "name": "area 1", - "type": AreaType.AREA.value, + "type": "AREA", "metadata": {"country": "FR", "tags": ["a"]}, }, ) + assert res.status_code == 200, res.json() + + # Test area creation with duplicate name res = client.post( f"/v1/studies/{study_id}/areas", headers=admin_headers, json={ - "name": "area 1", - "type": AreaType.AREA.value, + "name": "Area 1", # Same name but with different case + "type": "AREA", "metadata": {"country": "FR"}, }, ) - assert res.status_code == 500 + assert res.status_code == 409, res.json() assert res.json() == { - "description": "Area 'area 1' already exists and could not be created", - "exception": "CommandApplicationError", + "description": "Area 'Area 1' already exists and could not be created", + "exception": "DuplicateAreaName", } client.post( @@ -444,7 +447,7 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: headers=admin_headers, json={ "name": "area 2", - "type": AreaType.AREA.value, + "type": "AREA", "metadata": {"country": "DE"}, }, ) From 8b672d361f8a2a78b1c505e4e52fa97e1d883ca9 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 4 Mar 2024 10:24:14 +0100 Subject: [PATCH 06/11] fix(ui-results): adjust date times for accurate frequency display --- .../explore/Results/ResultDetails/index.tsx | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index ab74bd3a83..e1c3748b73 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -153,11 +153,36 @@ function ResultDetails() { const dateTimeFromIndex = useMemo(() => { if (!matrixRes.data) return []; - return matrixRes.data.index.map((dateTime) => { - const parsedDate = moment(dateTime, "MM/DD HH:mm"); - return parsedDate.format("ddd D MMM HH:mm"); - }); - }, [matrixRes.data]); + // Annual format has a static string + if (timestep === Timestep.Annual) { + return ["Annual"]; + } + + // Original date/time format mapping for moment parsing + const parseFormat = { + [Timestep.Hourly]: "MM/DD HH:mm", + [Timestep.Daily]: "MM/DD", + [Timestep.Weekly]: "WW", + [Timestep.Monthly]: "MM", + }[timestep]; + + // Output formats for each timestep to match legacy UI requirements + const outputFormat = { + [Timestep.Hourly]: "DD MMM HH:mm I", + [Timestep.Daily]: "DD MMM I", + [Timestep.Weekly]: "WW", + [Timestep.Monthly]: "MMM", + }[timestep]; + + const needsIndex = + timestep === Timestep.Hourly || timestep === Timestep.Daily; + + return matrixRes.data.index.map((dateTime, i) => + moment(dateTime, parseFormat).format( + outputFormat.replace("I", needsIndex ? ` - ${i + 1}` : ""), + ), + ); + }, [matrixRes.data, timestep]); //////////////////////////////////////////////////////////////// // Event Handlers From 7721747e29d114d07ccf1ccae842afe3e1faacf4 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:31:28 +0100 Subject: [PATCH 07/11] fix(ui-common): download latest value in MatrixInput component (#1962) --- .../components/common/MatrixInput/index.tsx | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/webapp/src/components/common/MatrixInput/index.tsx b/webapp/src/components/common/MatrixInput/index.tsx index 0d43522296..1bc3d00f33 100644 --- a/webapp/src/components/common/MatrixInput/index.tsx +++ b/webapp/src/components/common/MatrixInput/index.tsx @@ -25,6 +25,7 @@ import ImportDialog from "../dialogs/ImportDialog"; import MatrixAssignDialog from "./MatrixAssignDialog"; import { downloadMatrix } from "../../../utils/matrixUtils"; import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; +import { LoadingButton } from "@mui/lab"; const logErr = debug("antares:createimportform:error"); @@ -56,29 +57,16 @@ function MatrixInput({ const [t] = useTranslation(); const [openImportDialog, setOpenImportDialog] = useState(false); const [openMatrixAsignDialog, setOpenMatrixAsignDialog] = useState(false); + const [isDownloading, setIsDownloading] = useState(false); const { data: matrixData, isLoading, reload: reloadMatrix, - } = usePromiseWithSnackbarError( - async () => { - const res = fetchFn - ? await fetchFn(study.id) - : await getStudyData(study.id, url); - if (typeof res === "string") { - const fixed = res - .replace(/NaN/g, '"NaN"') - .replace(/Infinity/g, '"Infinity"'); - return JSON.parse(fixed); - } - return res; - }, - { - errorMessage: t("data.error.matrix"), - deps: [study, url], - }, - ); + } = usePromiseWithSnackbarError(fetchMatrixData, { + errorMessage: t("data.error.matrix"), + deps: [study.id, url, fetchFn], + }); const { data: matrixIndex } = usePromiseWithSnackbarError( async () => { @@ -99,6 +87,23 @@ function MatrixInput({ */ const rowNames = fetchFn ? matrixIndex : initialRowNames; + //////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////// + + async function fetchMatrixData() { + const res = fetchFn + ? await fetchFn(study.id) + : await getStudyData(study.id, url); + if (typeof res === "string") { + const fixed = res + .replace(/NaN/g, '"NaN"') + .replace(/Infinity/g, '"Infinity"'); + return JSON.parse(fixed); + } + return res; + } + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// @@ -133,8 +138,14 @@ function MatrixInput({ } }; - const handleDownload = (matrixData: MatrixType, fileName: string): void => { - downloadMatrix(matrixData, fileName); + const handleDownload = async (matrixData: MatrixType, fileName: string) => { + setIsDownloading(true); + + // Re-fetch to get latest data + const data = await fetchMatrixData(); + downloadMatrix(data, fileName); + + setIsDownloading(false); }; //////////////////////////////////////////////////////////////// @@ -179,10 +190,12 @@ function MatrixInput({ {matrixData?.columns?.length >= 1 && ( - + )} From fa0d3824d086fbb5523543d1506c2010c17ca520 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:29:30 +0100 Subject: [PATCH 08/11] feat(ui-common): add `keepLastResolvedOnReload` prop in usePromiseCond component --- .../common/utils/UsePromiseCond.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/common/utils/UsePromiseCond.tsx b/webapp/src/components/common/utils/UsePromiseCond.tsx index eca855245e..c103341375 100644 --- a/webapp/src/components/common/utils/UsePromiseCond.tsx +++ b/webapp/src/components/common/utils/UsePromiseCond.tsx @@ -41,6 +41,7 @@ export interface UsePromiseCondProps { ifPending?: () => React.ReactNode; ifRejected?: (error: Response["error"]) => React.ReactNode; ifResolved?: (data: T) => React.ReactNode; + keepLastResolvedOnReload?: boolean; } function UsePromiseCond(props: UsePromiseCondProps) { @@ -49,12 +50,31 @@ function UsePromiseCond(props: UsePromiseCondProps) { ifPending = () => , ifRejected = (error) => , ifResolved, + keepLastResolvedOnReload = false, } = props; const { status, data, error } = response; + //////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////// + + const hasToKeepLastResolved = () => { + return data !== undefined && keepLastResolvedOnReload; + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + return ( <> {R.cond([ + // Resolved + [ + R.either(R.equals(PromiseStatus.Resolved), hasToKeepLastResolved), + () => ifResolved?.(data as T), + ], + // Pending [ R.either( R.equals(PromiseStatus.Idle), @@ -62,8 +82,8 @@ function UsePromiseCond(props: UsePromiseCondProps) { ), () => ifPending(), ], + // Rejected [R.equals(PromiseStatus.Rejected), () => ifRejected(error)], - [R.equals(PromiseStatus.Resolved), () => ifResolved?.(data as T)], ])(status)} ); From c82be822b0020e88b6cdffd498523c960f992bcb Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:29:59 +0100 Subject: [PATCH 09/11] feat(ui-tasks): auto refresh launcher metrics --- .../src/components/App/Tasks/JobTableView.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/webapp/src/components/App/Tasks/JobTableView.tsx b/webapp/src/components/App/Tasks/JobTableView.tsx index 1381489764..506c95762e 100644 --- a/webapp/src/components/App/Tasks/JobTableView.tsx +++ b/webapp/src/components/App/Tasks/JobTableView.tsx @@ -31,6 +31,7 @@ import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarEr import { getLauncherMetrics } from "../../../services/api/study"; import LinearProgressWithLabel from "../../common/LinearProgressWithLabel"; import UsePromiseCond from "../../common/utils/UsePromiseCond"; +import { useInterval } from "react-use"; interface PropType { content: TaskView[]; @@ -46,13 +47,10 @@ function JobTableView(props: PropType) { useState(false); const [currentContent, setCurrentContent] = useState(content); - const laucherMetrics = usePromiseWithSnackbarError( - () => getLauncherMetrics(), - { - errorMessage: t("study.error.launchLoad"), - deps: [], - }, - ); + const launcherMetrics = usePromiseWithSnackbarError(getLauncherMetrics, { + errorMessage: t("study.error.launchLoad"), + deps: [], + }); const applyFilter = useCallback( (taskList: TaskView[]) => { @@ -91,6 +89,9 @@ function JobTableView(props: PropType) { setCurrentContent(applyFilter(content)); }, [content, applyFilter]); + // Refresh launcher metrics every minute + useInterval(launcherMetrics.reload, 60_000); + return ( ( <> {t("study.allocatedCpuRate")} @@ -152,7 +154,7 @@ function JobTableView(props: PropType) { color="primary" onClick={() => { refresh(); - laucherMetrics.reload(); + launcherMetrics.reload(); }} variant="outlined" > From 65f9ed7d0eca886f94b4206b06b952319b04914c Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:35:53 +0100 Subject: [PATCH 10/11] build: prepare hotfix release v2.16.6 (2024-03-04) --- antarest/__init__.py | 4 ++-- docs/CHANGELOG.md | 18 +++++++++++++++++- setup.py | 2 +- sonar-project.properties | 2 +- webapp/package-lock.json | 4 ++-- webapp/package.json | 2 +- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/antarest/__init__.py b/antarest/__init__.py index c3d6be21fb..d91081051d 100644 --- a/antarest/__init__.py +++ b/antarest/__init__.py @@ -7,9 +7,9 @@ # Standard project metadata -__version__ = "2.16.5" +__version__ = "2.16.6" __author__ = "RTE, Antares Web Team" -__date__ = "2024-02-29" +__date__ = "2024-03-04" # noinspection SpellCheckingInspection __credits__ = "(c) Réseau de Transport de l’Électricité (RTE)" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b4dbdda4e0..d93ac70454 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,22 @@ Antares Web Changelog ===================== +v2.16.6 (2024-03-04) +-------------------- + +### Features + +* **ui-tasks:** add launcher metrics [`#1960`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1960) +* **ui-tasks:** auto refresh launcher metrics [`#1963`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1963) + + +### Bug Fixes + +* **ui-results:** adjust date times for accurate frequency display [`#1960`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1960) +* **ui-common:** matrices display issues [`#1960`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1960) +* **ui-common:** download latest value of matrices [`#1962`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1962) + + v2.16.5 (2024-02-29) -------------------- @@ -18,7 +34,7 @@ v2.16.5 (2024-02-29) * **ui-hydro:** disable stretch to fix display issue on some matrices [`#1945`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1945) * **variants:** correct the generation of variant when a snapshot is removed [`#1947`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1947) * **tags:** resolve issue with `study.additional_data.patch` attribute reading [`#1944`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1944) -* **study:** correct access to study `additional_data` (#1949) [`#1949`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1949) +* **study:** correct access to study `additional_data` [`#1949`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1949) * **ui-tablemode:** create modal is frozen when submitting without column [`#1946`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1946) * **ui-tablemode:** 'co2' column not working [`#1952`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1952) * **ui:** add missing i18n dependency [`#1954`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1954) diff --git a/setup.py b/setup.py index 1e0d76dfe5..1446416f8d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="AntaREST", - version="2.16.5", + version="2.16.6", description="Antares Server", long_description=Path("README.md").read_text(encoding="utf-8"), long_description_content_type="text/markdown", diff --git a/sonar-project.properties b/sonar-project.properties index beb967df7e..643d9b727e 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,5 +6,5 @@ sonar.exclusions=antarest/gui.py,antarest/main.py sonar.python.coverage.reportPaths=coverage.xml sonar.python.version=3.8 sonar.javascript.lcov.reportPaths=webapp/coverage/lcov.info -sonar.projectVersion=2.16.5 +sonar.projectVersion=2.16.6 sonar.coverage.exclusions=antarest/gui.py,antarest/main.py,antarest/singleton_services.py,antarest/worker/archive_worker_service.py,webapp/**/* \ No newline at end of file diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 8a6261057b..48e281133d 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -1,12 +1,12 @@ { "name": "antares-web", - "version": "2.16.5", + "version": "2.16.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "antares-web", - "version": "2.16.5", + "version": "2.16.6", "dependencies": { "@emotion/react": "11.11.1", "@emotion/styled": "11.11.0", diff --git a/webapp/package.json b/webapp/package.json index be76665a99..4ea77417ac 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "antares-web", - "version": "2.16.5", + "version": "2.16.6", "private": true, "type": "module", "scripts": { From f7f082a0e836c81d0791fbd316eeb324a1d99088 Mon Sep 17 00:00:00 2001 From: MartinBelthle <102529366+MartinBelthle@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:03:01 +0100 Subject: [PATCH 11/11] fix(comments): use a command to update comments on a variant (#1959) Co-authored-by: Laurent LAPORTE --- antarest/study/service.py | 34 ++++----- antarest/study/storage/storage_service.py | 11 +-- .../model/command/update_raw_file.py | 9 +++ .../variant_blueprint/test_variant_manager.py | 72 ++++++++++++++++--- 4 files changed, 87 insertions(+), 39 deletions(-) diff --git a/antarest/study/service.py b/antarest/study/service.py index dc3288b4e2..c13f755aad 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -84,7 +84,6 @@ MatrixIndex, PatchArea, PatchCluster, - PatchStudy, RawStudy, Study, StudyAdditionalData, @@ -121,6 +120,7 @@ upgrade_study, ) from antarest.study.storage.utils import assert_permission, get_start_date, is_managed, remove_from_cache +from antarest.study.storage.variantstudy.business.utils import transform_command_to_dto from antarest.study.storage.variantstudy.model.command.icommand import ICommand from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix from antarest.study.storage.variantstudy.model.command.update_comments import UpdateComments @@ -395,17 +395,7 @@ def get_comments(self, study_id: str, params: RequestParameters) -> t.Union[str, study = self.get_study(study_id) assert_permission(params.user, study, StudyPermissionType.READ) - output: t.Union[str, JSON] - raw_study_service = self.storage_service.raw_study_service - variant_study_service = self.storage_service.variant_study_service - if isinstance(study, RawStudy): - output = raw_study_service.get(metadata=study, url="/settings/comments") - elif isinstance(study, VariantStudy): - patch = raw_study_service.patch_service.get(study) - patch_study = PatchStudy() if patch.study is None else patch.study - output = patch_study.comments or variant_study_service.get(metadata=study, url="/settings/comments") - else: - raise StudyTypeUnsupported(study.id, study.type) + output = self.storage_service.get_storage(study).get(metadata=study, url="/settings/comments") with contextlib.suppress(AttributeError, UnicodeDecodeError): output = output.decode("utf-8") # type: ignore @@ -440,14 +430,20 @@ def edit_comments( new=bytes(data.comments, "utf-8"), params=params, ) - elif isinstance(study, VariantStudy): - patch = self.storage_service.raw_study_service.patch_service.get(study) - patch_study = patch.study or PatchStudy() - patch_study.comments = data.comments - patch.study = patch_study - self.storage_service.raw_study_service.patch_service.save(study, patch) else: - raise StudyTypeUnsupported(study.id, study.type) + variant_study_service = self.storage_service.variant_study_service + command = [ + UpdateRawFile( + target="settings/comments", + b64Data=base64.b64encode(data.comments.encode("utf-8")).decode("utf-8"), + command_context=variant_study_service.command_factory.command_context, + ) + ] + variant_study_service.append_commands( + study.id, + transform_command_to_dto(command, force_aggregate=True), + RequestParameters(user=params.user), + ) def get_studies_information( self, diff --git a/antarest/study/storage/storage_service.py b/antarest/study/storage/storage_service.py index affe97eae1..599e948948 100644 --- a/antarest/study/storage/storage_service.py +++ b/antarest/study/storage/storage_service.py @@ -5,7 +5,6 @@ from typing import Union -from antarest.core.exceptions import StudyTypeUnsupported from antarest.study.common.studystorage import IStudyStorageService from antarest.study.model import RawStudy, Study from antarest.study.storage.rawstudy.raw_study_service import RawStudyService @@ -49,13 +48,5 @@ def get_storage(self, study: Study) -> IStudyStorageService[Union[RawStudy, Vari Returns: The study storage service associated with the study type. - - Raises: - StudyTypeUnsupported: If the study type is not supported by the available storage services. """ - if isinstance(study, RawStudy): - return self.raw_study_service - elif isinstance(study, VariantStudy): - return self.variant_study_service - else: - raise StudyTypeUnsupported(study.id, study.type) + return self.raw_study_service if isinstance(study, RawStudy) else self.variant_study_service diff --git a/antarest/study/storage/variantstudy/model/command/update_raw_file.py b/antarest/study/storage/variantstudy/model/command/update_raw_file.py index c4b6cfb46b..3e7b3b8759 100644 --- a/antarest/study/storage/variantstudy/model/command/update_raw_file.py +++ b/antarest/study/storage/variantstudy/model/command/update_raw_file.py @@ -26,6 +26,15 @@ class UpdateRawFile(ICommand): target: str b64Data: str + def __repr__(self) -> str: + cls = self.__class__.__name__ + target = self.target + try: + data = base64.decodebytes(self.b64Data.encode("utf-8")).decode("utf-8") + return f"{cls}(target={target!r}, data={data!r})" + except (ValueError, TypeError): + return f"{cls}(target={target!r}, b64Data={self.b64Data!r})" + def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: return CommandOutput(status=True, message="ok"), {} diff --git a/tests/integration/variant_blueprint/test_variant_manager.py b/tests/integration/variant_blueprint/test_variant_manager.py index 5af256dbbe..df3cf590e4 100644 --- a/tests/integration/variant_blueprint/test_variant_manager.py +++ b/tests/integration/variant_blueprint/test_variant_manager.py @@ -1,21 +1,45 @@ import logging +import typing as t +import pytest from starlette.testclient import TestClient from antarest.core.tasks.model import TaskDTO, TaskStatus -def test_variant_manager(client: TestClient, admin_access_token: str, study_id: str, caplog) -> None: +@pytest.fixture(name="base_study_id") +def base_study_id_fixture(client: TestClient, admin_access_token: str, caplog: t.Any) -> str: + """Create a base study and return its ID.""" + admin_headers = {"Authorization": f"Bearer {admin_access_token}"} with caplog.at_level(level=logging.WARNING): - admin_headers = {"Authorization": f"Bearer {admin_access_token}"} - - base_study_res = client.post("/v1/studies?name=foo", headers=admin_headers) + res = client.post("/v1/studies?name=Base1", headers=admin_headers) + return t.cast(str, res.json()) + + +@pytest.fixture(name="variant_id") +def variant_id_fixture( + client: TestClient, + admin_access_token: str, + base_study_id: str, + caplog: t.Any, +) -> str: + """Create a variant and return its ID.""" + admin_headers = {"Authorization": f"Bearer {admin_access_token}"} + with caplog.at_level(level=logging.WARNING): + res = client.post(f"/v1/studies/{base_study_id}/variants?name=Variant1", headers=admin_headers) + return t.cast(str, res.json()) - base_study_id = base_study_res.json() - res = client.post(f"/v1/studies/{base_study_id}/variants?name=foo", headers=admin_headers) - variant_id = res.json() +def test_variant_manager( + client: TestClient, + admin_access_token: str, + base_study_id: str, + variant_id: str, + caplog: t.Any, +) -> None: + admin_headers = {"Authorization": f"Bearer {admin_access_token}"} + with caplog.at_level(level=logging.WARNING): client.post(f"/v1/launcher/run/{variant_id}", headers=admin_headers) res = client.get(f"v1/studies/{variant_id}/synthesis", headers=admin_headers) @@ -26,9 +50,9 @@ def test_variant_manager(client: TestClient, admin_access_token: str, study_id: client.post(f"/v1/studies/{variant_id}/variants?name=baz", headers=admin_headers) res = client.get(f"/v1/studies/{base_study_id}/variants", headers=admin_headers) children = res.json() - assert children["node"]["name"] == "foo" + assert children["node"]["name"] == "Base1" assert len(children["children"]) == 1 - assert children["children"][0]["node"]["name"] == "foo" + assert children["children"][0]["node"]["name"] == "Variant1" assert len(children["children"][0]["children"]) == 2 assert children["children"][0]["children"][0]["node"]["name"] == "bar" assert children["children"][0]["children"][1]["node"]["name"] == "baz" @@ -169,7 +193,7 @@ def test_variant_manager(client: TestClient, admin_access_token: str, study_id: res = client.post(f"/v1/studies/{variant_id}/freeze?name=bar", headers=admin_headers) assert res.status_code == 500 - new_study_id = "newid" + new_study_id = "new_id" res = client.get(f"/v1/studies/{new_study_id}", headers=admin_headers) assert res.status_code == 404 @@ -186,3 +210,31 @@ def test_variant_manager(client: TestClient, admin_access_token: str, study_id: res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers) assert res.status_code == 404 + + +def test_comments(client: TestClient, admin_access_token: str, variant_id: str) -> None: + admin_headers = {"Authorization": f"Bearer {admin_access_token}"} + + # Put comments + comment = "updated comment" + res = client.put(f"/v1/studies/{variant_id}/comments", json={"comments": comment}, headers=admin_headers) + assert res.status_code == 204 + + # Asserts comments are updated + res = client.get(f"/v1/studies/{variant_id}/comments", headers=admin_headers) + assert res.json() == comment + + # Generates the study + res = client.put(f"/v1/studies/{variant_id}/generate?denormalize=false&from_scratch=true", headers=admin_headers) + task_id = res.json() + # Wait for task completion + res = client.get(f"/v1/tasks/{task_id}", headers=admin_headers, params={"wait_for_completion": True}) + assert res.status_code == 200 + task_result = TaskDTO.parse_obj(res.json()) + assert task_result.status == TaskStatus.COMPLETED + assert task_result.result is not None + assert task_result.result.success + + # Asserts comments did not disappear + res = client.get(f"/v1/studies/{variant_id}/comments", headers=admin_headers) + assert res.json() == comment