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": { 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/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..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 @@ -380,7 +405,6 @@ function ResultDetails() { matrix={matrix} matrixTime={false} rowNames={dateTimeFromIndex} - stretch={false} readOnly /> ) diff --git a/webapp/src/components/App/Tasks/JobTableView.tsx b/webapp/src/components/App/Tasks/JobTableView.tsx index 6a2003507e..506c95762e 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,10 @@ 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"; +import { useInterval } from "react-use"; interface PropType { content: TaskView[]; @@ -44,11 +47,10 @@ function JobTableView(props: PropType) { useState(false); const [currentContent, setCurrentContent] = useState(content); - const { data: load, reload: reloadLauncherLoad } = - usePromiseWithSnackbarError(() => getLauncherLoad(), { - errorMessage: t("study.error.launchLoad"), - deps: [], - }); + const launcherMetrics = usePromiseWithSnackbarError(getLauncherMetrics, { + errorMessage: t("study.error.launchLoad"), + deps: [], + }); const applyFilter = useCallback( (taskList: TaskView[]) => { @@ -87,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.clusterLoad")} - {load && ( - - )} + ( + <> + {t("study.allocatedCpuRate")} + + {t("study.clusterLoadRate")} + + + {t("study.nbQueuedJobs")}: {data.nbQueuedJobs} + + + )} + ifPending={() => } + /> @@ -131,7 +154,7 @@ function JobTableView(props: PropType) { color="primary" onClick={() => { refresh(); - reloadLauncherLoad(); + launcherMetrics.reload(); }} variant="outlined" > diff --git a/webapp/src/components/common/EditableMatrix/index.tsx b/webapp/src/components/common/EditableMatrix/index.tsx index 87aa8441f2..3e17ed5aaa 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; @@ -178,10 +176,9 @@ function EditableMatrix(props: PropTypes) { data={grid} width="100%" height="100%" - stretchH={stretch ? "all" : "none"} className="editableMatrix" colHeaders - rowHeaderWidth={matrixRowNames ? 150 : undefined} + rowHeaderWidth={rowNames ? 150 : 50} afterChange={(change, source) => onUpdate && handleSlice(change || [], source) } diff --git a/webapp/src/components/common/MatrixInput/index.tsx b/webapp/src/components/common/MatrixInput/index.tsx index 7807cd94c4..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"); @@ -38,7 +39,6 @@ interface Props { fetchFn?: fetchMatrixFn; disableEdit?: boolean; enablePercentDisplay?: boolean; - stretch?: boolean; } function MatrixInput({ @@ -51,36 +51,22 @@ function MatrixInput({ fetchFn, disableEdit, enablePercentDisplay, - stretch, }: Props) { const { enqueueSnackbar } = useSnackbar(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); 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 () => { @@ -101,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 //////////////////////////////////////////////////////////////// @@ -135,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); }; //////////////////////////////////////////////////////////////// @@ -181,10 +190,12 @@ function MatrixInput({ {matrixData?.columns?.length >= 1 && ( - + )} @@ -213,7 +224,6 @@ function MatrixInput({ onUpdate={handleUpdate} computStats={computStats} isPercentDisplayEnabled={enablePercentDisplay} - stretch={stretch} /> ) : ( !isLoading && ( 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)} ); 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; };