From e132c58cafbf93d2d850cd444c51366c451c4ac0 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Wed, 15 Jun 2022 21:22:27 +0200 Subject: [PATCH 01/24] Update version to 2.5.1 Signed-off-by: Paul Bui-Quang --- antarest/__init__.py | 2 +- setup.py | 2 +- sonar-project.properties | 2 +- webapp/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/antarest/__init__.py b/antarest/__init__.py index a87a359d4c..3736a3ba3b 100644 --- a/antarest/__init__.py +++ b/antarest/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.5.0" +__version__ = "2.5.1" from pathlib import Path diff --git a/setup.py b/setup.py index f708681741..98ea7b806a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="AntaREST", - version="2.5.0", + version="2.5.1", description="Antares Server", long_description=long_description, long_description_content_type="text/markdown", diff --git a/sonar-project.properties b/sonar-project.properties index f651d49bc8..58628f8ce8 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,5 +5,5 @@ sonar.language=python, js sonar.exclusions=antarest/gui.py,antarest/main.py sonar.python.coverage.reportPaths=coverage.xml sonar.javascript.lcov.reportPaths=webapp/coverage/lcov.info -sonar.projectVersion=2.5.0 +sonar.projectVersion=2.5.1 sonar.coverage.exclusions=antarest/gui.py,antarest/main.py,webapp/**/* \ No newline at end of file diff --git a/webapp/package.json b/webapp/package.json index db252b2aff..7fa2c81505 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "antares-web", - "version": "2.5.0", + "version": "2.5.1", "private": true, "dependencies": { "@emotion/react": "11.9.0", From 855de6e97ba65ad15e5157f06a35217a7da3c543 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Fri, 17 Jun 2022 11:15:47 +0200 Subject: [PATCH 02/24] Multiple fixes (#954) --- webapp/.eslintrc.json | 5 +- webapp/public/locales/en/main.json | 2 +- webapp/public/locales/fr/main.json | 2 +- .../components/common/MatrixInput/index.tsx | 8 +- .../components/common/SnackErrorMessage.tsx | 2 + webapp/src/components/common/TagTextInput.tsx | 3 +- .../common/dialogs/DataViewerDialog/index.tsx | 4 +- .../common/fieldEditors/BooleanFE.tsx | 2 + .../fieldEditors/ColorPickerFE/index.tsx | 2 + .../common/fieldEditors/SelectFE.tsx | 2 + .../common/fieldEditors/SwitchFE.tsx | 2 + webapp/src/components/data/DataListing.tsx | 2 + .../components/data/DatasetCreationDialog.tsx | 2 + .../src/components/settings/Tokens/index.tsx | 3 +- .../DraggableCommands/CommandListView.tsx | 6 +- .../Configuration/General/Fields/index.tsx | 31 +- .../explore/Configuration/General/index.tsx | 3 +- .../Modelization/Areas/Properties/index.tsx | 1 - .../StudyMatrixView/StudyMatrixView.tsx | 4 +- .../Modelization/Links/LinkView/index.tsx | 1 - .../explore/Modelization/Map/LinksView.tsx | 2 + .../explore/Results/ResultDetails/index.tsx | 4 +- .../singlestudy/explore/Results/index.tsx | 411 +++++++++--------- .../explore/Xpansion/Candidates/index.tsx | 7 +- .../src/components/studies/LauncherDialog.tsx | 2 +- .../studies/StudiesList/StudyCardCell.tsx | 2 + .../components/studies/StudiesList/index.tsx | 3 +- webapp/src/components/studies/StudyCard.tsx | 2 + webapp/src/hooks/useDebounce.ts | 32 +- webapp/src/hooks/useDebouncedEffect.ts | 35 ++ webapp/src/hooks/usePromise.ts | 15 +- .../src/hooks/usePromiseWithSnackbarError.ts | 7 +- webapp/src/pages/wrappers/LoginWrapper.tsx | 4 +- webapp/src/utils/reactUtils.ts | 5 + 34 files changed, 351 insertions(+), 267 deletions(-) create mode 100644 webapp/src/hooks/useDebouncedEffect.ts create mode 100644 webapp/src/utils/reactUtils.ts diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json index eb591242b7..890b2dede6 100644 --- a/webapp/.eslintrc.json +++ b/webapp/.eslintrc.json @@ -5,10 +5,11 @@ }, "extends": [ "eslint:recommended", + "airbnb", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:react/jsx-runtime", - "airbnb", + "plugin:react-hooks/recommended", "airbnb/hooks", "plugin:prettier/recommended" ], @@ -33,7 +34,6 @@ "ecmaVersion": 2018, "sourceType": "module" }, - "plugins": ["react", "react-hooks", "@typescript-eslint"], "rules": { "@typescript-eslint/no-shadow": "off", "@typescript-eslint/no-unused-vars": [ @@ -56,7 +56,6 @@ } ], "import/prefer-default-export": "off", - "no-undef": "off", "no-param-reassign": [ "error", { diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index af38707f26..535295c87d 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -225,7 +225,7 @@ "study.modelization.links.type": "Type", "study.modelization.links.transmissionCapa": "Transmission capacities", "study.modelization.links.transmissionCapa.infinite": "Infinite", - "study.modelization.links.transmissionCapa.ignore": "Ignore", + "study.modelization.links.transmissionCapa.ignore": "Null", "study.modelization.links.transmissionCapa.enabled": "Enabled", "study.modelization.links.type.ac": "AC", "study.modelization.links.type.dc": "DC", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 98064bb2af..1e1189140b 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -225,7 +225,7 @@ "study.modelization.links.type": "Type", "study.modelization.links.transmissionCapa": "Transmission capacities", "study.modelization.links.transmissionCapa.infinite": "Infinite", - "study.modelization.links.transmissionCapa.ignore": "Ignore", + "study.modelization.links.transmissionCapa.ignore": "Null", "study.modelization.links.transmissionCapa.enabled": "Enabled", "study.modelization.links.type.ac": "AC", "study.modelization.links.type.dc": "DC", diff --git a/webapp/src/components/common/MatrixInput/index.tsx b/webapp/src/components/common/MatrixInput/index.tsx index 9e2ccb2fd2..8d8a14a08f 100644 --- a/webapp/src/components/common/MatrixInput/index.tsx +++ b/webapp/src/components/common/MatrixInput/index.tsx @@ -55,16 +55,16 @@ function MatrixInput(props: PropsType) { }, { errorMessage: t("data.error.matrix"), - }, - [study, url] + deps: [study, url], + } ); const { data: matrixIndex } = usePromiseWithSnackbarError( () => getStudyMatrixIndex(study.id, url), { errorMessage: t("matrix.error.failedtoretrieveindex"), - }, - [study, url] + deps: [study, url], + } ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/common/SnackErrorMessage.tsx b/webapp/src/components/common/SnackErrorMessage.tsx index 8d2718d7b7..b732ae7a38 100644 --- a/webapp/src/components/common/SnackErrorMessage.tsx +++ b/webapp/src/components/common/SnackErrorMessage.tsx @@ -138,4 +138,6 @@ const SnackErrorMessage = forwardRef( } ); +SnackErrorMessage.displayName = "SnackErrorMessage"; + export default SnackErrorMessage; diff --git a/webapp/src/components/common/TagTextInput.tsx b/webapp/src/components/common/TagTextInput.tsx index 848e0f54b0..eb1fcebddf 100644 --- a/webapp/src/components/common/TagTextInput.tsx +++ b/webapp/src/components/common/TagTextInput.tsx @@ -1,5 +1,3 @@ -/* eslint-disable react/jsx-props-no-spreading */ -/* eslint-disable no-use-before-define */ import * as React from "react"; import Chip from "@mui/material/Chip"; import Autocomplete from "@mui/material/Autocomplete"; @@ -38,6 +36,7 @@ function TagTextInput(props: Props) { }} renderTags={(value: readonly string[], getTagProps) => value.map((option: string, index: number) => ( + // eslint-disable-next-line react/jsx-key )) } diff --git a/webapp/src/components/common/dialogs/DataViewerDialog/index.tsx b/webapp/src/components/common/dialogs/DataViewerDialog/index.tsx index 5eee1ab1cb..c05b5fbc9c 100644 --- a/webapp/src/components/common/dialogs/DataViewerDialog/index.tsx +++ b/webapp/src/components/common/dialogs/DataViewerDialog/index.tsx @@ -54,8 +54,8 @@ function DataViewerDialog(props: Props) { }, { errorMessage: t("matrix.error.failedToRetrieveIndex"), - }, - [studyId] + deps: [studyId], + } ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/common/fieldEditors/BooleanFE.tsx b/webapp/src/components/common/fieldEditors/BooleanFE.tsx index 692167ffe1..3152ac196b 100644 --- a/webapp/src/components/common/fieldEditors/BooleanFE.tsx +++ b/webapp/src/components/common/fieldEditors/BooleanFE.tsx @@ -63,4 +63,6 @@ const BooleanFE = forwardRef((props: BooleanFEProps, ref) => { ); }); +BooleanFE.displayName = "BooleanFE"; + export default BooleanFE; diff --git a/webapp/src/components/common/fieldEditors/ColorPickerFE/index.tsx b/webapp/src/components/common/fieldEditors/ColorPickerFE/index.tsx index 81166bbc97..c5d7962007 100644 --- a/webapp/src/components/common/fieldEditors/ColorPickerFE/index.tsx +++ b/webapp/src/components/common/fieldEditors/ColorPickerFE/index.tsx @@ -121,4 +121,6 @@ const ColorPicker = forwardRef((props: Props & TextFieldProps, ref) => { ); }); +ColorPicker.displayName = "ColorPicker"; + export default ColorPicker; diff --git a/webapp/src/components/common/fieldEditors/SelectFE.tsx b/webapp/src/components/common/fieldEditors/SelectFE.tsx index 4caf24cb28..2728a72b70 100644 --- a/webapp/src/components/common/fieldEditors/SelectFE.tsx +++ b/webapp/src/components/common/fieldEditors/SelectFE.tsx @@ -79,4 +79,6 @@ const SelectFE = forwardRef((props: SelectFEProps, ref) => { ); }); +SelectFE.displayName = "SelectFE"; + export default SelectFE; diff --git a/webapp/src/components/common/fieldEditors/SwitchFE.tsx b/webapp/src/components/common/fieldEditors/SwitchFE.tsx index 947f352b89..d7c5b3e33b 100644 --- a/webapp/src/components/common/fieldEditors/SwitchFE.tsx +++ b/webapp/src/components/common/fieldEditors/SwitchFE.tsx @@ -55,4 +55,6 @@ const SwitchFE = forwardRef((props: SwitchFEProps, ref) => { return fieldEditor; }); +SwitchFE.displayName = "SwitchFE"; + export default SwitchFE; diff --git a/webapp/src/components/data/DataListing.tsx b/webapp/src/components/data/DataListing.tsx index 6032055855..8639d5b873 100644 --- a/webapp/src/components/data/DataListing.tsx +++ b/webapp/src/components/data/DataListing.tsx @@ -72,6 +72,8 @@ const Row = memo((props: ListChildComponentProps) => { ); }, areEqual); +Row.displayName = "Row"; + function DataListing(props: PropsType) { const { datasets = [], selectedItem, setSelectedItem } = props; diff --git a/webapp/src/components/data/DatasetCreationDialog.tsx b/webapp/src/components/data/DatasetCreationDialog.tsx index 8acf26533a..799bf6831c 100644 --- a/webapp/src/components/data/DatasetCreationDialog.tsx +++ b/webapp/src/components/data/DatasetCreationDialog.tsx @@ -38,6 +38,8 @@ const HelperIcon = forwardRef((props, ref) => { return
; }); +HelperIcon.displayName = "HelperIcon"; + function DatasetCreationDialog(props: PropTypes) { const [t] = useTranslation(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); diff --git a/webapp/src/components/settings/Tokens/index.tsx b/webapp/src/components/settings/Tokens/index.tsx index 64805f8f91..f7c3632525 100644 --- a/webapp/src/components/settings/Tokens/index.tsx +++ b/webapp/src/components/settings/Tokens/index.tsx @@ -123,8 +123,7 @@ function Tokens() { const user = await getUser(authUser.id); return bots.map((bot) => ({ ...bot, user })); }, - { errorMessage: t("settings.error.tokensError") }, - [authUser] + { errorMessage: t("settings.error.tokensError"), deps: [authUser] } ); useUpdateEffect(() => { diff --git a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx b/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx index 887b813e8f..fa6ab6ab1f 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx +++ b/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import { memo, useEffect, useRef } from "react"; import { FixedSizeList, areEqual, ListChildComponentProps } from "react-window"; import { DragDropContext, @@ -9,7 +9,7 @@ import { import { CommandItem } from "../commandTypes"; import CommandListItem from "./CommandListItem"; -const Row = React.memo((props: ListChildComponentProps) => { +const Row = memo((props: ListChildComponentProps) => { const { data, index, style } = props; const { items, @@ -48,6 +48,8 @@ const Row = React.memo((props: ListChildComponentProps) => { ); }, areEqual); +Row.displayName = "Row"; + export type DraggableListProps = { items: CommandItem[]; generationStatus: boolean; diff --git a/webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx b/webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx index aedd662ce0..fecb2257b5 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx +++ b/webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx @@ -15,6 +15,7 @@ import { } from "../utils"; import BooleanFE from "../../../../../common/fieldEditors/BooleanFE"; import { useFormContext } from "../../../../../common/Form"; +import useDebouncedEffect from "../../../../../../hooks/useDebouncedEffect"; interface Props { study: StudyMetadata; @@ -27,7 +28,11 @@ function Fields(props: Props) { const studyVersion = Number(study.version); const [t] = useTranslation(); const { register, setValue, watch, getValues } = useFormContext(); - const buildingMode = watch("buildingMode"); + const [buildingMode, firstDay, lastDay] = watch([ + "buildingMode", + "firstDay", + "lastDay", + ]); useEffect(() => { if (buildingMode === "Derated") { @@ -35,16 +40,34 @@ function Fields(props: Props) { } }, [buildingMode, setValue]); + useDebouncedEffect( + () => { + if (firstDay > 0 && firstDay > lastDay) { + setValue("lastDay", firstDay); + } + }, + { wait: 500, deps: [firstDay] } + ); + + useDebouncedEffect( + () => { + if (lastDay > 0 && lastDay < firstDay) { + setValue("firstDay", lastDay); + } + }, + { wait: 500, deps: [lastDay] } + ); + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// const handleDayValidation = (v: number) => { - if (v === 0 || Number.isNaN(v)) { + if (v < 1 || Number.isNaN(v)) { return "Minimum is 1"; } if (getValues("firstDay") > getValues("lastDay")) { - return "First day must be lower or equal to last day"; + return false; } if (getValues("leapYear")) { return v <= 366 ? true : "Maximum is 366 for a leap year"; @@ -155,7 +178,7 @@ function Fields(props: Props) { ? true : "Value must be 1 when building mode is derated"; } - if (v === 0) { + if (v < 1) { return "Minimum is 1"; } return v <= 50000 ? true : "Maximum is 50000"; diff --git a/webapp/src/components/singlestudy/explore/Configuration/General/index.tsx b/webapp/src/components/singlestudy/explore/Configuration/General/index.tsx index cc8a8f40e4..4ad78a4069 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/General/index.tsx +++ b/webapp/src/components/singlestudy/explore/Configuration/General/index.tsx @@ -13,8 +13,7 @@ function GeneralParameters() { const { data, status, error } = usePromiseWithSnackbarError( () => getFormValues(study.id), - { errorMessage: "Cannot get study data" }, // TODO i18n - [study.id] + { errorMessage: "Cannot get study data", deps: [study.id] } // TODO i18n ); return R.cond([ diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx b/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx index f583bf5ee7..a6ea7dc419 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx +++ b/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx @@ -17,7 +17,6 @@ function Properties() { const [t] = useTranslation(); const { data: defaultValues, status } = usePromise( () => getDefaultValues(study.id, currentArea, t), - {}, [study.id, currentArea] ); diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx b/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx index f0dcf5d1c6..e988667ea5 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx +++ b/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx @@ -49,8 +49,8 @@ function StudyMatrixView(props: PropTypes) { () => getStudyMatrixIndex(study, formatedPath), { errorMessage: t("matrix.error.failedToRetrieveIndex"), - }, - [study, formatedPath] + deps: [study, formatedPath], + } ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx b/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx index a15ed3271e..3f02ad24d3 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx +++ b/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx @@ -17,7 +17,6 @@ function LinkView(props: Props) { const { link } = props; const { data: defaultValues, status } = usePromise( () => getDefaultValues(study.id, link.area1, link.area2), - {}, [study.id, link.area1, link.area2] ); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx b/webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx index fb61e364b2..44920b71e4 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx +++ b/webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx @@ -48,6 +48,8 @@ const Row = memo((props: ListChildComponentProps) => { ); }, areEqual); +Row.displayName = "Row"; + function LinksView(props: PropsType) { const { links, node, setSelectedItem } = props; const [t] = useTranslation(); diff --git a/webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx index cf6dd1a054..4bf987e2f6 100644 --- a/webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx @@ -14,8 +14,8 @@ function ResultDetails() { return ( (); const { t } = useTranslation(); @@ -97,18 +85,42 @@ function Results() { const { data: studyJobs, isLoading: studyJobsLoading } = usePromiseWithSnackbarError(() => getStudyJobs(study.id), { errorMessage: t("results.error.jobs"), + deps: [study.id], }); const { data: studyOutputs, isLoading: studyOutputsLoading } = usePromiseWithSnackbarError(() => getStudyOutputs(study.id), { errorMessage: t("results.error.outputs"), + deps: [study.id], }); + const outputs = useMemo(() => { + if (studyJobs && studyOutputs) { + return combineJobsAndOutputs(studyJobs, studyOutputs).sort((a, b) => { + if (!a.completionDate || !b.completionDate) { + if (!a.completionDate && !b.completionDate) { + return moment(a.creationDate).isAfter(moment(b.creationDate)) + ? -1 + : 1; + } + if (!a.completionDate) { + return -1; + } + return 1; + } + return moment(a.completionDate).isAfter(moment(b.completionDate)) + ? -1 + : 1; + }); + } + return []; + }, [studyJobs, studyOutputs]); + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// - const handleOutputNameClick = (outputName: string) => { + const handleOutputNameClick = (outputName: string) => () => { navigate(`/studies/${study.id}/explore/results/${outputName}`); }; @@ -116,19 +128,17 @@ function Results() { // JSX //////////////////////////////////////////////////////////////// - const outputs = combineJobsAndOutputs(studyJobs || [], studyOutputs || []); - return ( - +
- { - R.cond([ - [ - () => studyJobsLoading && studyOutputsLoading, - () => ( - <> - {Array.from({ length: 3 }, (v, k) => k).map((v) => ( - td, &:last-child > th": { - border: 0, - }, - }} - > - - - - - ))} - - ), - ], - [ - () => !!(outputs && outputs.length > 0), - () => ( - <> - {outputs - .sort((a, b) => { - if (!a.completionDate || !b.completionDate) { - if (!a.completionDate && !b.completionDate) { - return moment(a.creationDate).isAfter( - moment(b.creationDate) - ) - ? -1 - : 1; - } - if (!a.completionDate) { - return -1; - } - return 1; - } - return moment(a.completionDate).isAfter( - moment(b.completionDate) - ) - ? -1 - : 1; - }) - .map((row) => ( - studyJobsLoading && studyOutputsLoading, + () => ( + <> + {Array.from({ length: 3 }, (v, k) => k).map((v) => ( + td, &:last-child > th": { + border: 0, + }, + }} + > + + + + + ))} + + ), + ], + [ + () => outputs.length > 0, + () => ( + <> + {outputs.map((row) => ( + td, &:last-child > th": { + border: 0, + }, + }} + > + + {row.completionDate ? ( + + {row.name} + + ) : ( + + + {row.name} + + )} + + + td, &:last-child > th": { - border: 0, - }, + display: "flex", + alignItems: "flex-end", + justifyContent: "center", + flexDirection: "column", + color: grey[500], + fontSize: "0.85rem", }} > - - {row.completionDate ? ( - + - handleOutputNameClick(row.name) - } - > - {row.name} - - ) : ( - - + {convertUTCToLocalTime(row.creationDate)} + + )} + + {row.completionDate && ( + <> + - {row.name} - + {convertUTCToLocalTime(row.completionDate)} + )} - - - - {row.creationDate && ( - - - {convertUTCToLocalTime(row.creationDate)} - - )} - - {row.completionDate && ( - <> - - {convertUTCToLocalTime( - row.completionDate - )} - - )} - - - - - - - {row.completionDate && row.job ? ( - - { - if (row.job) { - downloadJobOutput(row.job.id); - } - }} - /> - - ) : ( - - )} - - {row.job && ( - + + + + + + {row.completionDate && row.job ? ( + + { + if (row.job) { + downloadJobOutput(row.job.id); + } + }} /> - )} - - - - ))} - - ), - ], - [ - R.T, - () => ( - td, &:last-child > th": { - border: 0, - }, - }} - > - - - {t("results.noOutputs")} - - - - ), - ], - ])() as ReactNode - } + + ) : ( + + )} + + {row.job && ( + + )} + + + + ))} + + ), + ], + [ + R.T, + () => ( + td, &:last-child > th": { + border: 0, + }, + }} + > + + + {t("results.noOutputs")} + + + + ), + ], + ])()}
diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx b/webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx index 5eb1ece05b..a40b78b2f7 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx +++ b/webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx @@ -76,8 +76,8 @@ function Candidates() { { errorMessage: t("xpansion.error.loadConfiguration"), resetDataOnReload: false, - }, - [study] + deps: [study], + } ); const { data: capaLinks } = usePromiseWithSnackbarError( @@ -94,8 +94,7 @@ function Candidates() { } return {}; }, - { errorMessage: t("xpansion.error.loadConfiguration") }, - [study] + { errorMessage: t("xpansion.error.loadConfiguration"), deps: [study] } ); const deleteXpansion = async () => { diff --git a/webapp/src/components/studies/LauncherDialog.tsx b/webapp/src/components/studies/LauncherDialog.tsx index a9584e6bd7..d5dd25a001 100644 --- a/webapp/src/components/studies/LauncherDialog.tsx +++ b/webapp/src/components/studies/LauncherDialog.tsx @@ -171,7 +171,7 @@ function LauncherDialog(props: Props) { ) : ( {studyNames.map((name) => ( - + {name} ))} diff --git a/webapp/src/components/studies/StudiesList/StudyCardCell.tsx b/webapp/src/components/studies/StudiesList/StudyCardCell.tsx index c9f146b521..48b2ebbb2b 100644 --- a/webapp/src/components/studies/StudiesList/StudyCardCell.tsx +++ b/webapp/src/components/studies/StudiesList/StudyCardCell.tsx @@ -69,4 +69,6 @@ const StudyCardCell = memo( } ); +StudyCardCell.displayName = "StudyCardCell"; + export default StudyCardCell; diff --git a/webapp/src/components/studies/StudiesList/index.tsx b/webapp/src/components/studies/StudiesList/index.tsx index cad9078db7..17e3190eb6 100644 --- a/webapp/src/components/studies/StudiesList/index.tsx +++ b/webapp/src/components/studies/StudiesList/index.tsx @@ -123,8 +123,7 @@ function StudiesList(props: StudiesListProps) { (scrollProp: GridOnScrollProps) => { dispatch(setStudyScrollPosition(scrollProp.scrollTop)); }, - 400, - { trailing: true } + { wait: 400, trailing: true } ); const handleToggleSelectStudy = useCallback((sid: string) => { diff --git a/webapp/src/components/studies/StudyCard.tsx b/webapp/src/components/studies/StudyCard.tsx index d703150bd4..89778d0c42 100644 --- a/webapp/src/components/studies/StudyCard.tsx +++ b/webapp/src/components/studies/StudyCard.tsx @@ -542,4 +542,6 @@ const StudyCard = memo((props: Props) => { ); }, areEqual); +StudyCard.displayName = "StudyCard"; + export default StudyCard; diff --git a/webapp/src/hooks/useDebounce.ts b/webapp/src/hooks/useDebounce.ts index d8775f4549..d79a522a12 100644 --- a/webapp/src/hooks/useDebounce.ts +++ b/webapp/src/hooks/useDebounce.ts @@ -1,12 +1,34 @@ -import { debounce, DebouncedFunc, DebounceSettings } from "lodash"; +import { + debounce, + DebouncedFunc, + DebouncedFuncLeading, + DebounceSettings, + DebounceSettingsLeading, +} from "lodash"; +import * as R from "ramda"; +import * as RA from "ramda-adjunct"; import { useEffect, useMemo, useRef } from "react"; import { F } from "ts-toolbelt"; -function useDebounce( +export interface UseDebounceParams extends DebounceSettings { + wait?: number; +} + +type WaitOrParams = number | UseDebounceParams; + +const toParams = R.cond<[WaitOrParams | undefined], UseDebounceParams>([ + [RA.isPlainObj, R.identity as () => UseDebounceParams], + [RA.isNumber, R.objOf("wait") as () => UseDebounceParams], + [R.T, RA.stubObj], +]); + +function useDebounce( fn: T, - wait?: number, - options?: DebounceSettings -): DebouncedFunc { + params?: U +): U extends DebounceSettingsLeading + ? DebouncedFuncLeading + : DebouncedFunc { + const { wait, ...options } = toParams(params); const fnRef = useRef(fn); useEffect(() => { diff --git a/webapp/src/hooks/useDebouncedEffect.ts b/webapp/src/hooks/useDebouncedEffect.ts new file mode 100644 index 0000000000..838c05ce7a --- /dev/null +++ b/webapp/src/hooks/useDebouncedEffect.ts @@ -0,0 +1,35 @@ +import * as R from "ramda"; +import * as RA from "ramda-adjunct"; +import { useEffect } from "react"; +import useDebounce, { UseDebounceParams } from "./useDebounce"; + +export interface UseDebouncedEffectParams extends UseDebounceParams { + deps?: React.DependencyList; +} + +type DepsOrWaitOrParams = + | React.DependencyList + | number + | UseDebouncedEffectParams; + +const toParams = R.cond< + [DepsOrWaitOrParams | undefined], + UseDebouncedEffectParams +>([ + [RA.isPlainObj, R.identity as () => UseDebouncedEffectParams], + [RA.isNumber, R.objOf("wait") as () => UseDebouncedEffectParams], + [RA.isArray, R.objOf("deps") as () => UseDebouncedEffectParams], + [R.T, RA.stubObj], +]); + +function useDebouncedEffect( + effect: VoidFunction, + params?: DepsOrWaitOrParams +): void { + const { deps, ...debounceParams } = toParams(params); + const debouncedFn = useDebounce(effect, debounceParams); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(debouncedFn, deps); +} + +export default useDebouncedEffect; diff --git a/webapp/src/hooks/usePromise.ts b/webapp/src/hooks/usePromise.ts index e377b087e0..c43f25e6d7 100644 --- a/webapp/src/hooks/usePromise.ts +++ b/webapp/src/hooks/usePromise.ts @@ -1,5 +1,6 @@ -import { DependencyList, useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { usePromise as usePromiseWrapper } from "react-use"; +import { isDependencyList } from "../utils/reactUtils"; export enum PromiseStatus { Idle = "idle", @@ -21,20 +22,26 @@ export interface UsePromiseResponse { export interface UsePromiseParams { resetDataOnReload?: boolean; resetErrorOnReload?: boolean; + deps?: React.DependencyList; +} + +type DepsOrParams = React.DependencyList | UsePromiseParams; + +function toParams(value: DepsOrParams = {}): UsePromiseParams { + return isDependencyList(value) ? { deps: value } : value; } function usePromise( fn: () => Promise, - params: UsePromiseParams = {}, - deps: DependencyList = [] + params?: DepsOrParams ): UsePromiseResponse { + const { deps = [], resetDataOnReload, resetErrorOnReload } = toParams(params); const [data, setData] = useState(); const [status, setStatus] = useState(PromiseStatus.Idle); const [error, setError] = useState(); const [reloadCount, setReloadCount] = useState(0); const reload = useCallback(() => setReloadCount((prev) => prev + 1), []); const mounted = usePromiseWrapper(); - const { resetDataOnReload, resetErrorOnReload } = params; useEffect( () => { diff --git a/webapp/src/hooks/usePromiseWithSnackbarError.ts b/webapp/src/hooks/usePromiseWithSnackbarError.ts index 4a21e07334..bc94974861 100644 --- a/webapp/src/hooks/usePromiseWithSnackbarError.ts +++ b/webapp/src/hooks/usePromiseWithSnackbarError.ts @@ -1,4 +1,4 @@ -import { DependencyList, useEffect } from "react"; +import { useEffect } from "react"; import useEnqueueErrorSnackbar from "./useEnqueueErrorSnackbar"; import usePromise, { UsePromiseResponse, UsePromiseParams } from "./usePromise"; @@ -8,10 +8,9 @@ export interface UsePromiseWithSnackbarErrorParams extends UsePromiseParams { function usePromiseWithSnackbarError( fn: () => Promise, - params: UsePromiseWithSnackbarErrorParams, - deps: DependencyList = [] + params: UsePromiseWithSnackbarErrorParams ): UsePromiseResponse { - const res = usePromise(fn, params, deps); + const res = usePromise(fn, params); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const { errorMessage } = params; diff --git a/webapp/src/pages/wrappers/LoginWrapper.tsx b/webapp/src/pages/wrappers/LoginWrapper.tsx index d1d4556082..9d5014d291 100644 --- a/webapp/src/pages/wrappers/LoginWrapper.tsx +++ b/webapp/src/pages/wrappers/LoginWrapper.tsx @@ -60,8 +60,8 @@ function LoginWrapper(props: Props) { { errorMessage: t("login.error"), resetDataOnReload: false, - }, - [user] + deps: [user], + } ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/utils/reactUtils.ts b/webapp/src/utils/reactUtils.ts new file mode 100644 index 0000000000..b5031ffce8 --- /dev/null +++ b/webapp/src/utils/reactUtils.ts @@ -0,0 +1,5 @@ +export function isDependencyList( + value: unknown +): value is React.DependencyList { + return Array.isArray(value); +} From cd55221c3b4099a0730115ac419026b409a3dc62 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Fri, 17 Jun 2022 11:52:48 +0200 Subject: [PATCH 03/24] Move page components (#955) --- webapp/src/{pages => components/App}/Api.tsx | 2 +- .../{data => App/Data}/DataListing.tsx | 2 +- .../{data => App/Data}/DataPropsView.tsx | 4 +- .../Data}/DatasetCreationDialog.tsx | 10 +-- .../{data => App/Data}/MatrixDialog.tsx | 8 +-- .../App/Data/index.tsx} | 26 +++---- .../components/{data => App/Data}/styles.ts | 0 .../components/{data => App/Data}/utils.tsx | 4 +- .../Settings}/Groups/Header.tsx | 6 +- .../Groups/dialog/CreateGroupDialog.tsx | 8 +-- .../dialog/GroupFormDialog/GroupForm.tsx | 14 ++-- .../Groups/dialog/GroupFormDialog/index.tsx | 4 +- .../Groups/dialog/UpdateGroupDialog.tsx | 8 +-- .../Settings}/Groups/index.tsx | 16 ++--- .../Settings}/Maintenance/index.tsx | 8 +-- .../Settings}/Tokens/Header.tsx | 2 +- .../Tokens/dialog/CreateTokenDialog.tsx | 10 +-- .../dialog/TokenFormDialog/TokenForm.tsx | 15 ++-- .../Tokens/dialog/TokenFormDialog/index.tsx | 2 +- .../Tokens/dialog/TokenInfoDialog.tsx | 4 +- .../Settings}/Tokens/index.tsx | 16 ++--- .../Settings}/Users/Header.tsx | 2 +- .../Users/dialog/CreateUserDialog.tsx | 8 +-- .../Users/dialog/UpdateUserDialog.tsx | 12 ++-- .../Users/dialog/UserFormDialog/UserForm.tsx | 10 +-- .../Users/dialog/UserFormDialog/index.tsx | 4 +- .../Settings}/Users/index.tsx | 12 ++-- .../App/Settings/index.tsx} | 17 +++-- .../{settings => App/Settings}/utils.ts | 2 +- .../Commands/Edition/AddCommandDialog.tsx | 2 +- .../DraggableCommands/CommandImportButton.tsx | 0 .../CommandListItem/index.tsx | 4 +- .../CommandListItem/style.ts | 2 +- .../DraggableCommands/CommandListView.tsx | 0 .../Commands/Edition/commandTypes.ts | 2 +- .../Singlestudy}/Commands/Edition/index.tsx | 14 ++-- .../Singlestudy}/Commands/Edition/style.ts | 0 .../Singlestudy}/Commands/Edition/utils.ts | 2 +- .../Singlestudy}/Commands/index.tsx | 0 .../Singlestudy}/Commands/style.ts | 0 .../CreateVariantModal/index.tsx | 12 ++-- .../CreateVariantModal/style.ts | 0 .../LauncherHistory/JobStepper.tsx | 12 ++-- .../InformationView/LauncherHistory/index.tsx | 8 +-- .../InformationView/LauncherHistory/style.ts | 0 .../Notes/NodeEditorModal/index.tsx | 2 +- .../Notes/NodeEditorModal/style.ts | 0 .../HomeView/InformationView/Notes/index.tsx | 8 +-- .../HomeView/InformationView/Notes/utils.ts | 0 .../HomeView/InformationView/index.tsx | 10 +-- .../Singlestudy}/HomeView/Split.css | 0 .../HomeView/StudyTreeView/index.tsx | 2 +- .../HomeView/StudyTreeView/treeconfig.ts | 0 .../HomeView/StudyTreeView/utils.ts | 2 +- .../Singlestudy}/HomeView/index.tsx | 2 +- .../Singlestudy}/NavHeader.tsx | 26 +++---- .../Singlestudy}/PropertiesDialog/index.tsx | 18 ++--- .../Singlestudy}/PropertiesDialog/style.ts | 0 .../AdvancedParameters/index.tsx | 2 +- .../Configuration/General/Fields/index.tsx | 14 ++-- .../explore/Configuration/General/index.tsx | 10 +-- .../explore/Configuration/General/styles.ts | 2 +- .../explore/Configuration/General/utils.ts | 4 +- .../OptimizationPreferences/index.tsx | 2 +- .../Configuration/RegionalDistricts/index.tsx | 2 +- .../TimeSeriesManagement/index.tsx | 2 +- .../explore/Configuration/index.tsx | 4 +- .../Modelization/Areas/AreaPropsView.tsx | 10 +-- .../explore/Modelization/Areas/AreasTab.tsx | 2 +- .../Modelization/Areas/Hydro/index.tsx | 2 +- .../explore/Modelization/Areas/Load.tsx | 8 +-- .../explore/Modelization/Areas/MiscGen.tsx | 8 +-- .../Areas/Properties/PropertiesForm.tsx | 16 ++--- .../Modelization/Areas/Properties/common.ts | 6 +- .../Modelization/Areas/Properties/index.tsx | 14 ++-- .../Modelization/Areas/Properties/utils.ts | 4 +- .../Modelization/Areas/Renewables/index.tsx | 8 +++ .../Modelization/Areas/Renewables/preview.png | Bin .../explore/Modelization/Areas/Reserve.tsx | 8 +-- .../explore/Modelization/Areas/Solar.tsx | 8 +-- .../Modelization/Areas/Thermal/index.tsx | 2 +- .../Modelization/Areas/Thermal/preview.png | Bin .../explore/Modelization/Areas/Wind.tsx | 8 +-- .../explore/Modelization/Areas/index.tsx | 16 ++--- .../BindingConstraints}/index.tsx | 4 +- .../BindingConstraints/preview.png | Bin .../DebugView/StudyDataView/StudyFileView.tsx | 11 +-- .../DebugView/StudyDataView/StudyJsonView.tsx | 9 ++- .../StudyMatrixView/StudyMatrixView.tsx | 21 +++--- .../StudyDataView/StudyMatrixView/style.ts | 0 .../DebugView/StudyDataView/index.tsx | 2 +- .../DebugView/StudyDataView/style.ts | 0 .../DebugView/StudyDataView/utils/utils.ts | 0 .../DebugView/StudyTreeView/index.tsx | 2 +- .../DebugView/StudyTreeView/utils.ts | 2 +- .../explore/Modelization/DebugView/index.tsx | 8 +-- .../Modelization/Links/LinkPropsView.tsx | 8 +-- .../Modelization/Links/LinkView/LinkForm.tsx | 16 ++--- .../Links/LinkView/LinkMatrixView.tsx | 4 +- .../Modelization/Links/LinkView/index.tsx | 10 +-- .../Modelization/Links/LinkView/utils.ts | 2 +- .../explore/Modelization/Links/index.tsx | 19 ++--- .../Modelization/Map/CreateAreaModal.tsx | 4 +- .../explore/Modelization/Map/GraphView.tsx | 2 +- .../explore/Modelization/Map/LinksView.tsx | 2 +- .../explore/Modelization/Map/MapPropsView.tsx | 4 +- .../explore/Modelization/Map/NodeView.tsx | 4 +- .../explore/Modelization/Map/PanelView.tsx | 4 +- .../explore/Modelization/Map/index.tsx | 18 ++--- .../Modelization/Map}/mapbackground.png | Bin .../explore/Modelization/index.tsx | 2 +- .../explore/Results/ResultDetails/index.tsx | 4 +- .../explore/Results/ResultDetails/preview.png | Bin .../Singlestudy}/explore/Results/index.tsx | 10 +-- .../Singlestudy}/explore/TabWrapper.tsx | 2 +- .../Xpansion/Candidates/CandidateForm.tsx | 6 +- .../Candidates/CreateCandidateDialog.tsx | 6 +- .../Xpansion/Candidates/XpansionPropsView.tsx | 4 +- .../explore/Xpansion/Candidates/index.tsx | 16 ++--- .../explore/Xpansion/Capacities/index.tsx | 12 ++-- .../explore/Xpansion/Files/index.tsx | 12 ++-- .../Xpansion/Settings/SettingsForm.tsx | 2 +- .../explore/Xpansion/Settings/index.tsx | 12 ++-- .../Singlestudy}/explore/Xpansion/index.tsx | 6 +- .../explore/Xpansion/share/styles.ts | 0 .../Singlestudy}/explore/Xpansion/types.ts | 0 .../explore/common/ListElement.tsx | 0 .../explore/hooks/useStudyData.ts | 10 +-- .../App/Singlestudy}/index.tsx | 24 +++---- .../Studies}/BatchModeMenu.tsx | 0 .../Studies}/CreateStudyDialog/index.tsx | 28 ++++---- .../Studies}/CreateStudyDialog/style.ts | 0 .../Filter/MultipleLinkElement/index.tsx | 4 +- .../Filter/MultipleLinkElement/style.ts | 0 .../Filter/SingleLinkElement/index.tsx | 2 +- .../Filter/SingleLinkElement/style.ts | 0 .../ExportModal/ExportFilter/Filter/index.tsx | 4 +- .../ExportModal/ExportFilter/Filter/style.ts | 0 .../ExportFilter/TagSelect/index.tsx | 0 .../ExportFilter/TagSelect/style.ts | 0 .../ExportModal/ExportFilter/index.tsx | 6 +- .../Studies}/ExportModal/index.tsx | 10 +-- .../{studies => App/Studies}/FilterDrawer.tsx | 14 ++-- .../{studies => App/Studies}/HeaderBottom.tsx | 14 ++-- .../Studies}/HeaderTopRight.tsx | 8 +-- .../Studies}/LauncherDialog.tsx | 12 ++-- .../Studies}/MoveStudyDialog.tsx | 12 ++-- .../{studies => App/Studies}/SideNav.tsx | 6 +- .../Studies}/StudiesList/StudyCardCell.tsx | 2 +- .../Studies}/StudiesList/index.tsx | 14 ++-- .../{studies => App/Studies}/StudyCard.tsx | 20 +++--- .../{studies => App/Studies}/StudyTree.tsx | 12 ++-- .../App}/Studies/index.tsx | 22 +++--- .../{studies => App/Studies}/utils.ts | 4 +- .../{tasks => App/Tasks}/JobTableView.tsx | 2 +- .../{tasks => App/Tasks}/LaunchJobLogView.tsx | 8 +-- .../Tasks}/NotificationBadge.tsx | 14 ++-- .../App/Tasks/index.tsx} | 36 +++++----- .../src/{App.tsx => components/App/index.tsx} | 66 +++++++++--------- .../Modelization/BindingConstraints/index.tsx | 8 --- .../wrappers/LoginWrapper.tsx | 4 +- .../MaintenanceWrapper/MessageInfoDialog.tsx | 2 +- .../wrappers/MaintenanceWrapper/Stars.css | 0 .../wrappers/MaintenanceWrapper/Stars.tsx | 0 .../wrappers/MaintenanceWrapper/index.tsx | 0 .../wrappers/MenuWrapper/index.tsx | 4 +- .../wrappers/MenuWrapper/styles.ts | 0 webapp/src/index.tsx | 2 +- webapp/src/redux/selectors.ts | 4 +- webapp/src/services/api/xpansion.ts | 2 +- .../utils.ts => utils/studiesUtils.ts} | 4 +- 171 files changed, 595 insertions(+), 565 deletions(-) rename webapp/src/{pages => components/App}/Api.tsx (91%) rename webapp/src/components/{data => App/Data}/DataListing.tsx (98%) rename webapp/src/components/{data => App/Data}/DataPropsView.tsx (92%) rename webapp/src/components/{data => App/Data}/DatasetCreationDialog.tsx (96%) rename webapp/src/components/{data => App/Data}/MatrixDialog.tsx (85%) rename webapp/src/{pages/Data.tsx => components/App/Data/index.tsx} (91%) rename webapp/src/components/{data => App/Data}/styles.ts (100%) rename webapp/src/components/{data => App/Data}/utils.tsx (97%) rename webapp/src/components/{settings => App/Settings}/Groups/Header.tsx (90%) rename webapp/src/components/{settings => App/Settings}/Groups/dialog/CreateGroupDialog.tsx (91%) rename webapp/src/components/{settings => App/Settings}/Groups/dialog/GroupFormDialog/GroupForm.tsx (93%) rename webapp/src/components/{settings => App/Settings}/Groups/dialog/GroupFormDialog/index.tsx (84%) rename webapp/src/components/{settings => App/Settings}/Groups/dialog/UpdateGroupDialog.tsx (94%) rename webapp/src/components/{settings => App/Settings}/Groups/index.tsx (93%) rename webapp/src/components/{settings => App/Settings}/Maintenance/index.tsx (93%) rename webapp/src/components/{settings => App/Settings}/Tokens/Header.tsx (97%) rename webapp/src/components/{settings => App/Settings}/Tokens/dialog/CreateTokenDialog.tsx (92%) rename webapp/src/components/{settings => App/Settings}/Tokens/dialog/TokenFormDialog/TokenForm.tsx (94%) rename webapp/src/components/{settings => App/Settings}/Tokens/dialog/TokenFormDialog/index.tsx (92%) rename webapp/src/components/{settings => App/Settings}/Tokens/dialog/TokenInfoDialog.tsx (89%) rename webapp/src/components/{settings => App/Settings}/Tokens/index.tsx (93%) rename webapp/src/components/{settings => App/Settings}/Users/Header.tsx (96%) rename webapp/src/components/{settings => App/Settings}/Users/dialog/CreateUserDialog.tsx (91%) rename webapp/src/components/{settings => App/Settings}/Users/dialog/UpdateUserDialog.tsx (90%) rename webapp/src/components/{settings => App/Settings}/Users/dialog/UserFormDialog/UserForm.tsx (95%) rename webapp/src/components/{settings => App/Settings}/Users/dialog/UserFormDialog/index.tsx (89%) rename webapp/src/components/{settings => App/Settings}/Users/index.tsx (94%) rename webapp/src/{pages/Settings.tsx => components/App/Settings/index.tsx} (84%) rename webapp/src/components/{settings => App/Settings}/utils.ts (82%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/Edition/AddCommandDialog.tsx (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/Edition/DraggableCommands/CommandImportButton.tsx (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/Edition/DraggableCommands/CommandListItem/index.tsx (98%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/Edition/DraggableCommands/CommandListItem/style.ts (99%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/Edition/DraggableCommands/CommandListView.tsx (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/Edition/commandTypes.ts (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/Edition/index.tsx (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/Edition/style.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/Edition/utils.ts (98%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/index.tsx (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/Commands/style.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/CreateVariantModal/index.tsx (85%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/CreateVariantModal/style.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/LauncherHistory/JobStepper.tsx (92%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/LauncherHistory/index.tsx (94%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/LauncherHistory/style.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/Notes/NodeEditorModal/index.tsx (98%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/Notes/NodeEditorModal/style.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/Notes/index.tsx (95%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/Notes/utils.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/InformationView/index.tsx (91%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/Split.css (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/StudyTreeView/index.tsx (98%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/StudyTreeView/treeconfig.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/StudyTreeView/utils.ts (98%) rename webapp/src/components/{singlestudy => App/Singlestudy}/HomeView/index.tsx (96%) rename webapp/src/components/{singlestudy => App/Singlestudy}/NavHeader.tsx (95%) rename webapp/src/components/{singlestudy => App/Singlestudy}/PropertiesDialog/index.tsx (93%) rename webapp/src/components/{singlestudy => App/Singlestudy}/PropertiesDialog/style.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Configuration/AdvancedParameters/index.tsx (56%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Configuration/General/Fields/index.tsx (94%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Configuration/General/index.tsx (71%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Configuration/General/styles.ts (84%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Configuration/General/utils.ts (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Configuration/OptimizationPreferences/index.tsx (59%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Configuration/RegionalDistricts/index.tsx (56%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Configuration/TimeSeriesManagement/index.tsx (57%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Configuration/index.tsx (92%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/AreaPropsView.tsx (82%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/AreasTab.tsx (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Hydro/index.tsx (65%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Load.tsx (56%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/MiscGen.tsx (66%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Properties/PropertiesForm.tsx (93%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Properties/common.ts (87%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Properties/index.tsx (74%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Properties/utils.ts (95%) create mode 100644 webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/index.tsx rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Renewables/preview.png (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Reserve.tsx (64%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Solar.tsx (56%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Thermal/index.tsx (65%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Thermal/preview.png (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/Wind.tsx (56%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Areas/index.tsx (81%) rename webapp/src/components/{singlestudy/explore/Modelization/Areas/Renewables => App/Singlestudy/explore/Modelization/BindingConstraints}/index.tsx (73%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/BindingConstraints/preview.png (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx (90%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx (92%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx (90%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/style.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/StudyDataView/index.tsx (95%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/StudyDataView/style.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/StudyDataView/utils/utils.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/StudyTreeView/index.tsx (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/StudyTreeView/utils.ts (93%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/DebugView/index.tsx (89%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Links/LinkPropsView.tsx (84%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Links/LinkView/LinkForm.tsx (93%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Links/LinkView/LinkMatrixView.tsx (95%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Links/LinkView/index.tsx (80%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Links/LinkView/utils.ts (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Links/index.tsx (80%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Map/CreateAreaModal.tsx (93%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Map/GraphView.tsx (96%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Map/LinksView.tsx (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Map/MapPropsView.tsx (96%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Map/NodeView.tsx (96%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Map/PanelView.tsx (98%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/Map/index.tsx (95%) rename webapp/src/{assets => components/App/Singlestudy/explore/Modelization/Map}/mapbackground.png (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Modelization/index.tsx (95%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Results/ResultDetails/index.tsx (87%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Results/ResultDetails/preview.png (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Results/index.tsx (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/TabWrapper.tsx (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/Candidates/CandidateForm.tsx (97%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/Candidates/CreateCandidateDialog.tsx (96%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/Candidates/XpansionPropsView.tsx (96%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/Candidates/index.tsx (92%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/Capacities/index.tsx (88%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/Files/index.tsx (88%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/Settings/SettingsForm.tsx (99%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/Settings/index.tsx (90%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/index.tsx (93%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/share/styles.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/Xpansion/types.ts (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/common/ListElement.tsx (100%) rename webapp/src/components/{singlestudy => App/Singlestudy}/explore/hooks/useStudyData.ts (76%) rename webapp/src/{pages/SingleStudy => components/App/Singlestudy}/index.tsx (85%) rename webapp/src/components/{studies => App/Studies}/BatchModeMenu.tsx (100%) rename webapp/src/components/{studies => App/Studies}/CreateStudyDialog/index.tsx (86%) rename webapp/src/components/{studies => App/Studies}/CreateStudyDialog/style.ts (100%) rename webapp/src/components/{studies => App/Studies}/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx (94%) rename webapp/src/components/{studies => App/Studies}/ExportModal/ExportFilter/Filter/MultipleLinkElement/style.ts (100%) rename webapp/src/components/{studies => App/Studies}/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx (94%) rename webapp/src/components/{studies => App/Studies}/ExportModal/ExportFilter/Filter/SingleLinkElement/style.ts (100%) rename webapp/src/components/{studies => App/Studies}/ExportModal/ExportFilter/Filter/index.tsx (96%) rename webapp/src/components/{studies => App/Studies}/ExportModal/ExportFilter/Filter/style.ts (100%) rename webapp/src/components/{studies => App/Studies}/ExportModal/ExportFilter/TagSelect/index.tsx (100%) rename webapp/src/components/{studies => App/Studies}/ExportModal/ExportFilter/TagSelect/style.ts (100%) rename webapp/src/components/{studies => App/Studies}/ExportModal/ExportFilter/index.tsx (97%) rename webapp/src/components/{studies => App/Studies}/ExportModal/index.tsx (95%) rename webapp/src/components/{studies => App/Studies}/FilterDrawer.tsx (92%) rename webapp/src/components/{studies => App/Studies}/HeaderBottom.tsx (91%) rename webapp/src/components/{studies => App/Studies}/HeaderTopRight.tsx (90%) rename webapp/src/components/{studies => App/Studies}/LauncherDialog.tsx (96%) rename webapp/src/components/{studies => App/Studies}/MoveStudyDialog.tsx (86%) rename webapp/src/components/{studies => App/Studies}/SideNav.tsx (88%) rename webapp/src/components/{studies => App/Studies}/StudiesList/StudyCardCell.tsx (97%) rename webapp/src/components/{studies => App/Studies}/StudiesList/index.tsx (96%) rename webapp/src/components/{studies => App/Studies}/StudyCard.tsx (96%) rename webapp/src/components/{studies => App/Studies}/StudyTree.tsx (92%) rename webapp/src/{pages => components/App}/Studies/index.tsx (75%) rename webapp/src/components/{studies => App/Studies}/utils.ts (94%) rename webapp/src/components/{tasks => App/Tasks}/JobTableView.tsx (99%) rename webapp/src/components/{tasks => App/Tasks}/LaunchJobLogView.tsx (93%) rename webapp/src/components/{tasks => App/Tasks}/NotificationBadge.tsx (88%) rename webapp/src/{pages/Tasks.tsx => components/App/Tasks/index.tsx} (93%) rename webapp/src/{App.tsx => components/App/index.tsx} (65%) delete mode 100644 webapp/src/components/singlestudy/explore/Modelization/BindingConstraints/index.tsx rename webapp/src/{pages => components}/wrappers/LoginWrapper.tsx (97%) rename webapp/src/{pages => components}/wrappers/MaintenanceWrapper/MessageInfoDialog.tsx (97%) rename webapp/src/{pages => components}/wrappers/MaintenanceWrapper/Stars.css (100%) rename webapp/src/{pages => components}/wrappers/MaintenanceWrapper/Stars.tsx (100%) rename webapp/src/{pages => components}/wrappers/MaintenanceWrapper/index.tsx (100%) rename webapp/src/{pages => components}/wrappers/MenuWrapper/index.tsx (98%) rename webapp/src/{pages => components}/wrappers/MenuWrapper/styles.ts (100%) rename webapp/src/{pages/Studies/utils.ts => utils/studiesUtils.ts} (96%) diff --git a/webapp/src/pages/Api.tsx b/webapp/src/components/App/Api.tsx similarity index 91% rename from webapp/src/pages/Api.tsx rename to webapp/src/components/App/Api.tsx index da098eab96..612194c0b1 100644 --- a/webapp/src/pages/Api.tsx +++ b/webapp/src/components/App/Api.tsx @@ -1,7 +1,7 @@ import { Box } from "@mui/material"; import SwaggerUI from "swagger-ui-react"; import "swagger-ui-react/swagger-ui.css"; -import { getConfig } from "../services/config"; +import { getConfig } from "../../services/config"; function Api() { return ( diff --git a/webapp/src/components/data/DataListing.tsx b/webapp/src/components/App/Data/DataListing.tsx similarity index 98% rename from webapp/src/components/data/DataListing.tsx rename to webapp/src/components/App/Data/DataListing.tsx index 8639d5b873..af6400b38a 100644 --- a/webapp/src/components/data/DataListing.tsx +++ b/webapp/src/components/App/Data/DataListing.tsx @@ -4,7 +4,7 @@ import { Typography, Box, styled } from "@mui/material"; import AutoSizer from "react-virtualized-auto-sizer"; import { FixedSizeList, areEqual, ListChildComponentProps } from "react-window"; import ArrowRightIcon from "@mui/icons-material/ArrowRight"; -import { MatrixDataSetDTO } from "../../common/types"; +import { MatrixDataSetDTO } from "../../../common/types"; const ROW_ITEM_SIZE = 45; const BUTTONS_SIZE = 40; diff --git a/webapp/src/components/data/DataPropsView.tsx b/webapp/src/components/App/Data/DataPropsView.tsx similarity index 92% rename from webapp/src/components/data/DataPropsView.tsx rename to webapp/src/components/App/Data/DataPropsView.tsx index 7fe7eba4d1..490d8379d2 100644 --- a/webapp/src/components/data/DataPropsView.tsx +++ b/webapp/src/components/App/Data/DataPropsView.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { MatrixDataSetDTO, MatrixInfoDTO } from "../../common/types"; -import PropertiesView from "../common/PropertiesView"; +import { MatrixDataSetDTO, MatrixInfoDTO } from "../../../common/types"; +import PropertiesView from "../../common/PropertiesView"; import DataListing from "./DataListing"; import { StyledListingBox } from "./styles"; diff --git a/webapp/src/components/data/DatasetCreationDialog.tsx b/webapp/src/components/App/Data/DatasetCreationDialog.tsx similarity index 96% rename from webapp/src/components/data/DatasetCreationDialog.tsx rename to webapp/src/components/App/Data/DatasetCreationDialog.tsx index 799bf6831c..2a97ef4e22 100644 --- a/webapp/src/components/data/DatasetCreationDialog.tsx +++ b/webapp/src/components/App/Data/DatasetCreationDialog.tsx @@ -12,12 +12,12 @@ import { useSnackbar } from "notistack"; import { useTranslation } from "react-i18next"; import axios, { AxiosError } from "axios"; import HelpIcon from "@mui/icons-material/Help"; -import { getGroups } from "../../services/api/user"; -import { GroupDTO, MatrixDataSetDTO } from "../../common/types"; +import { getGroups } from "../../../services/api/user"; +import { GroupDTO, MatrixDataSetDTO } from "../../../common/types"; import { saveMatrix } from "./utils"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../common/loaders/SimpleLoader"; -import BasicDialog from "../common/dialogs/BasicDialog"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../common/loaders/SimpleLoader"; +import BasicDialog from "../../common/dialogs/BasicDialog"; import { BoxParamHeader, BoxParam, ParamTitle } from "./styles"; interface PropTypes { diff --git a/webapp/src/components/data/MatrixDialog.tsx b/webapp/src/components/App/Data/MatrixDialog.tsx similarity index 85% rename from webapp/src/components/data/MatrixDialog.tsx rename to webapp/src/components/App/Data/MatrixDialog.tsx index f824ec05bd..ddd7a59b85 100644 --- a/webapp/src/components/data/MatrixDialog.tsx +++ b/webapp/src/components/App/Data/MatrixDialog.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; -import { MatrixInfoDTO, MatrixType } from "../../common/types"; -import { getMatrix } from "../../services/api/matrix"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import DataViewerDialog from "../common/dialogs/DataViewerDialog"; +import { MatrixInfoDTO, MatrixType } from "../../../common/types"; +import { getMatrix } from "../../../services/api/matrix"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import DataViewerDialog from "../../common/dialogs/DataViewerDialog"; interface PropTypes { matrixInfo: MatrixInfoDTO; diff --git a/webapp/src/pages/Data.tsx b/webapp/src/components/App/Data/index.tsx similarity index 91% rename from webapp/src/pages/Data.tsx rename to webapp/src/components/App/Data/index.tsx index 0d9295ff1f..4471856f04 100644 --- a/webapp/src/pages/Data.tsx +++ b/webapp/src/components/App/Data/index.tsx @@ -7,24 +7,24 @@ import StorageIcon from "@mui/icons-material/Storage"; import { Box, Typography, IconButton, Tooltip } from "@mui/material"; import EditIcon from "@mui/icons-material/Edit"; import DownloadIcon from "@mui/icons-material/Download"; -import DataPropsView from "../components/data/DataPropsView"; +import DataPropsView from "./DataPropsView"; import { deleteDataSet, exportMatrixDataset, getMatrixList, getExportMatrixUrl, -} from "../services/api/matrix"; -import { MatrixInfoDTO, MatrixDataSetDTO } from "../common/types"; -import DatasetCreationDialog from "../components/data/DatasetCreationDialog"; -import ConfirmationDialog from "../components/common/dialogs/ConfirmationDialog"; -import RootPage from "../components/common/page/RootPage"; -import MatrixDialog from "../components/data/MatrixDialog"; -import useEnqueueErrorSnackbar from "../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../components/common/loaders/SimpleLoader"; -import SplitLayoutView from "../components/common/SplitLayoutView"; -import FileTable from "../components/common/FileTable"; -import { getAuthUser } from "../redux/selectors"; -import useAppSelector from "../redux/hooks/useAppSelector"; +} from "../../../services/api/matrix"; +import { MatrixInfoDTO, MatrixDataSetDTO } from "../../../common/types"; +import DatasetCreationDialog from "./DatasetCreationDialog"; +import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +import RootPage from "../../common/page/RootPage"; +import MatrixDialog from "./MatrixDialog"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../common/loaders/SimpleLoader"; +import SplitLayoutView from "../../common/SplitLayoutView"; +import FileTable from "../../common/FileTable"; +import { getAuthUser } from "../../../redux/selectors"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; function Data() { const [t] = useTranslation(); diff --git a/webapp/src/components/data/styles.ts b/webapp/src/components/App/Data/styles.ts similarity index 100% rename from webapp/src/components/data/styles.ts rename to webapp/src/components/App/Data/styles.ts diff --git a/webapp/src/components/data/utils.tsx b/webapp/src/components/App/Data/utils.tsx similarity index 97% rename from webapp/src/components/data/utils.tsx rename to webapp/src/components/App/Data/utils.tsx index a578e6feda..baf52e158b 100644 --- a/webapp/src/components/data/utils.tsx +++ b/webapp/src/components/App/Data/utils.tsx @@ -3,12 +3,12 @@ import { MatrixDataSetDTO, MatrixDataSetUpdateDTO, MatrixInfoDTO, -} from "../../common/types"; +} from "../../../common/types"; import { createMatrixByImportation, updateDataSet, createDataSet, -} from "../../services/api/matrix"; +} from "../../../services/api/matrix"; const updateMatrix = async ( data: MatrixDataSetDTO, diff --git a/webapp/src/components/settings/Groups/Header.tsx b/webapp/src/components/App/Settings/Groups/Header.tsx similarity index 90% rename from webapp/src/components/settings/Groups/Header.tsx rename to webapp/src/components/App/Settings/Groups/Header.tsx index d6f1f7a0fe..511f4f3cea 100644 --- a/webapp/src/components/settings/Groups/Header.tsx +++ b/webapp/src/components/App/Settings/Groups/Header.tsx @@ -3,10 +3,10 @@ import GroupAddIcon from "@mui/icons-material/GroupAdd"; import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import { useState } from "react"; -import { GroupDetailsDTO } from "../../../common/types"; +import { GroupDetailsDTO } from "../../../../common/types"; import CreateGroupDialog from "./dialog/CreateGroupDialog"; -import { isAuthUserAdmin } from "../../../redux/selectors"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { isAuthUserAdmin } from "../../../../redux/selectors"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; /** * Types diff --git a/webapp/src/components/settings/Groups/dialog/CreateGroupDialog.tsx b/webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx similarity index 91% rename from webapp/src/components/settings/Groups/dialog/CreateGroupDialog.tsx rename to webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx index 4855044fc8..1b590492a7 100644 --- a/webapp/src/components/settings/Groups/dialog/CreateGroupDialog.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx @@ -7,10 +7,10 @@ import { GroupDTO, RoleDetailsDTO, UserDTO, -} from "../../../../common/types"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; -import { createGroup, createRole } from "../../../../services/api/user"; -import { SubmitHandlerData } from "../../../common/Form"; +} from "../../../../../common/types"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { createGroup, createRole } from "../../../../../services/api/user"; +import { SubmitHandlerData } from "../../../../common/Form"; import GroupFormDialog, { GroupFormDialogProps } from "./GroupFormDialog"; /** diff --git a/webapp/src/components/settings/Groups/dialog/GroupFormDialog/GroupForm.tsx b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx similarity index 93% rename from webapp/src/components/settings/Groups/dialog/GroupFormDialog/GroupForm.tsx rename to webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx index a9dd0dc75c..df592e5d7e 100644 --- a/webapp/src/components/settings/Groups/dialog/GroupFormDialog/GroupForm.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx @@ -28,13 +28,13 @@ import { RESERVED_USER_NAMES, ROLE_TYPE_KEYS, } from "../../../utils"; -import { RoleType, UserDTO } from "../../../../../common/types"; -import { roleToString, sortByName } from "../../../../../services/utils"; -import usePromise from "../../../../../hooks/usePromise"; -import { getUsers } from "../../../../../services/api/user"; -import { getAuthUser } from "../../../../../redux/selectors"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { FormObj } from "../../../../common/Form"; +import { RoleType, UserDTO } from "../../../../../../common/types"; +import { roleToString, sortByName } from "../../../../../../services/utils"; +import usePromise from "../../../../../../hooks/usePromise"; +import { getUsers } from "../../../../../../services/api/user"; +import { getAuthUser } from "../../../../../../redux/selectors"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { FormObj } from "../../../../../common/Form"; function GroupForm(props: FormObj) { const { diff --git a/webapp/src/components/settings/Groups/dialog/GroupFormDialog/index.tsx b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/index.tsx similarity index 84% rename from webapp/src/components/settings/Groups/dialog/GroupFormDialog/index.tsx rename to webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/index.tsx index f00bb42286..26ea1d5b65 100644 --- a/webapp/src/components/settings/Groups/dialog/GroupFormDialog/index.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/index.tsx @@ -1,7 +1,7 @@ import FormDialog, { FormDialogProps, -} from "../../../../common/dialogs/FormDialog"; -import { RoleType, UserDTO } from "../../../../../common/types"; +} from "../../../../../common/dialogs/FormDialog"; +import { RoleType, UserDTO } from "../../../../../../common/types"; import GroupForm from "./GroupForm"; /** diff --git a/webapp/src/components/settings/Groups/dialog/UpdateGroupDialog.tsx b/webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx similarity index 94% rename from webapp/src/components/settings/Groups/dialog/UpdateGroupDialog.tsx rename to webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx index 5a17619978..1b61989787 100644 --- a/webapp/src/components/settings/Groups/dialog/UpdateGroupDialog.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx @@ -4,17 +4,17 @@ import { useTranslation } from "react-i18next"; import { usePromise as usePromiseWrapper } from "react-use"; import { useSnackbar } from "notistack"; import * as R from "ramda"; -import { GroupDetailsDTO } from "../../../../common/types"; +import { GroupDetailsDTO } from "../../../../../common/types"; import { createRole, deleteUserRole, getRolesForGroup, updateGroup, -} from "../../../../services/api/user"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +} from "../../../../../services/api/user"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; import GroupFormDialog, { GroupFormDialogProps } from "./GroupFormDialog"; import { GroupEdit } from ".."; -import { SubmitHandlerData } from "../../../common/Form"; +import { SubmitHandlerData } from "../../../../common/Form"; type InheritPropsToOmit = | "title" diff --git a/webapp/src/components/settings/Groups/index.tsx b/webapp/src/components/App/Settings/Groups/index.tsx similarity index 93% rename from webapp/src/components/settings/Groups/index.tsx rename to webapp/src/components/App/Settings/Groups/index.tsx index 278b912f4f..d006e3e91e 100644 --- a/webapp/src/components/settings/Groups/index.tsx +++ b/webapp/src/components/App/Settings/Groups/index.tsx @@ -20,17 +20,17 @@ import EditIcon from "@mui/icons-material/Edit"; import * as R from "ramda"; import GroupIcon from "@mui/icons-material/Group"; import { useSnackbar } from "notistack"; -import { GroupDetailsDTO } from "../../../common/types"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; -import { deleteGroup, getGroups } from "../../../services/api/user"; -import { sortByName } from "../../../services/utils"; -import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import { GroupDetailsDTO } from "../../../../common/types"; +import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; +import { deleteGroup, getGroups } from "../../../../services/api/user"; +import { sortByName } from "../../../../services/utils"; +import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; import { RESERVED_GROUP_NAMES } from "../utils"; import Header from "./Header"; import UpdateGroupDialog from "./dialog/UpdateGroupDialog"; -import { getAuthUser } from "../../../redux/selectors"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getAuthUser } from "../../../../redux/selectors"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; /** * Types diff --git a/webapp/src/components/settings/Maintenance/index.tsx b/webapp/src/components/App/Settings/Maintenance/index.tsx similarity index 93% rename from webapp/src/components/settings/Maintenance/index.tsx rename to webapp/src/components/App/Settings/Maintenance/index.tsx index 9baa9983be..c1ecaec92a 100644 --- a/webapp/src/components/settings/Maintenance/index.tsx +++ b/webapp/src/components/App/Settings/Maintenance/index.tsx @@ -13,15 +13,15 @@ import { useState } from "react"; import { Controller, FieldValues, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useUpdateEffect } from "react-use"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; import { getMaintenanceMode, getMessageInfo, updateMaintenanceMode, updateMessageInfo, -} from "../../../services/api/maintenance"; -import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +} from "../../../../services/api/maintenance"; +import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; function Maintenance() { const { t } = useTranslation(); diff --git a/webapp/src/components/settings/Tokens/Header.tsx b/webapp/src/components/App/Settings/Tokens/Header.tsx similarity index 97% rename from webapp/src/components/settings/Tokens/Header.tsx rename to webapp/src/components/App/Settings/Tokens/Header.tsx index 18ba91b372..ae2c473783 100644 --- a/webapp/src/components/settings/Tokens/Header.tsx +++ b/webapp/src/components/App/Settings/Tokens/Header.tsx @@ -3,7 +3,7 @@ import TokenIcon from "@mui/icons-material/Token"; import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import { useState } from "react"; -import { BotDTO } from "../../../common/types"; +import { BotDTO } from "../../../../common/types"; import CreateTokenDialog from "./dialog/CreateTokenDialog"; /** diff --git a/webapp/src/components/settings/Tokens/dialog/CreateTokenDialog.tsx b/webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx similarity index 92% rename from webapp/src/components/settings/Tokens/dialog/CreateTokenDialog.tsx rename to webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx index 4447dd8b7e..9c847756ce 100644 --- a/webapp/src/components/settings/Tokens/dialog/CreateTokenDialog.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx @@ -10,12 +10,12 @@ import { BotDTO, GroupDTO, RoleType, -} from "../../../../common/types"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; -import { createBot } from "../../../../services/api/user"; -import OkDialog from "../../../common/dialogs/OkDialog"; +} from "../../../../../common/types"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { createBot } from "../../../../../services/api/user"; +import OkDialog from "../../../../common/dialogs/OkDialog"; import TokenFormDialog, { TokenFormDialogProps } from "./TokenFormDialog"; -import { SubmitHandlerData } from "../../../common/Form"; +import { SubmitHandlerData } from "../../../../common/Form"; type InheritPropsToOmit = "title" | "titleIcon" | "onSubmit" | "onCancel"; diff --git a/webapp/src/components/settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx similarity index 94% rename from webapp/src/components/settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx rename to webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx index 43a896ba79..91889d997f 100644 --- a/webapp/src/components/settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx @@ -24,13 +24,16 @@ import { v4 as uuidv4 } from "uuid"; import DeleteIcon from "@mui/icons-material/Delete"; import GroupIcon from "@mui/icons-material/Group"; import { TokenFormDialogProps } from "."; -import { GroupDTO, RoleType } from "../../../../../common/types"; -import usePromise from "../../../../../hooks/usePromise"; -import { getGroups } from "../../../../../services/api/user"; -import { roleToString, sortByName } from "../../../../../services/utils"; +import { GroupDTO, RoleType } from "../../../../../../common/types"; +import usePromise from "../../../../../../hooks/usePromise"; +import { getGroups } from "../../../../../../services/api/user"; +import { roleToString, sortByName } from "../../../../../../services/utils"; import { RESERVED_GROUP_NAMES, ROLE_TYPE_KEYS } from "../../../utils"; -import { getAuthUser, isAuthUserAdmin } from "../../../../../redux/selectors"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; +import { + getAuthUser, + isAuthUserAdmin, +} from "../../../../../../redux/selectors"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; interface Props extends UseFormReturn { onlyPermissions?: TokenFormDialogProps["onlyPermissions"]; diff --git a/webapp/src/components/settings/Tokens/dialog/TokenFormDialog/index.tsx b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/index.tsx similarity index 92% rename from webapp/src/components/settings/Tokens/dialog/TokenFormDialog/index.tsx rename to webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/index.tsx index aa6d5b9714..6da35bd3ad 100644 --- a/webapp/src/components/settings/Tokens/dialog/TokenFormDialog/index.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/index.tsx @@ -1,6 +1,6 @@ import FormDialog, { FormDialogProps, -} from "../../../../common/dialogs/FormDialog"; +} from "../../../../../common/dialogs/FormDialog"; import TokenForm from "./TokenForm"; /** diff --git a/webapp/src/components/settings/Tokens/dialog/TokenInfoDialog.tsx b/webapp/src/components/App/Settings/Tokens/dialog/TokenInfoDialog.tsx similarity index 89% rename from webapp/src/components/settings/Tokens/dialog/TokenInfoDialog.tsx rename to webapp/src/components/App/Settings/Tokens/dialog/TokenInfoDialog.tsx index 21dcd6080a..0bbe11b728 100644 --- a/webapp/src/components/settings/Tokens/dialog/TokenInfoDialog.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/TokenInfoDialog.tsx @@ -3,8 +3,8 @@ import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import InfoIcon from "@mui/icons-material/Info"; import { useMemo } from "react"; -import { BotDetailsDTO, RoleType, UserDTO } from "../../../../common/types"; -import OkDialog, { OkDialogProps } from "../../../common/dialogs/OkDialog"; +import { BotDetailsDTO, RoleType, UserDTO } from "../../../../../common/types"; +import OkDialog, { OkDialogProps } from "../../../../common/dialogs/OkDialog"; import TokenForm from "./TokenFormDialog/TokenForm"; /** diff --git a/webapp/src/components/settings/Tokens/index.tsx b/webapp/src/components/App/Settings/Tokens/index.tsx similarity index 93% rename from webapp/src/components/settings/Tokens/index.tsx rename to webapp/src/components/App/Settings/Tokens/index.tsx index f7c3632525..49b635b572 100644 --- a/webapp/src/components/settings/Tokens/index.tsx +++ b/webapp/src/components/App/Settings/Tokens/index.tsx @@ -20,21 +20,21 @@ import InfoIcon from "@mui/icons-material/Info"; import TokenIcon from "@mui/icons-material/Token"; import * as R from "ramda"; import { useSnackbar } from "notistack"; -import { BotDTO, BotDetailsDTO, UserDTO } from "../../../common/types"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; +import { BotDTO, BotDetailsDTO, UserDTO } from "../../../../common/types"; +import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; import { deleteBot, getBots, getUser, getUsers, -} from "../../../services/api/user"; -import { isUserAdmin, sortByProp } from "../../../services/utils"; -import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +} from "../../../../services/api/user"; +import { isUserAdmin, sortByProp } from "../../../../services/utils"; +import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; import Header from "./Header"; -import { getAuthUser } from "../../../redux/selectors"; +import { getAuthUser } from "../../../../redux/selectors"; import TokenInfoDialog from "./dialog/TokenInfoDialog"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; /** * Types diff --git a/webapp/src/components/settings/Users/Header.tsx b/webapp/src/components/App/Settings/Users/Header.tsx similarity index 96% rename from webapp/src/components/settings/Users/Header.tsx rename to webapp/src/components/App/Settings/Users/Header.tsx index 42421219e9..fcdbac23d2 100644 --- a/webapp/src/components/settings/Users/Header.tsx +++ b/webapp/src/components/App/Settings/Users/Header.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import { useState } from "react"; import CreateUserDialog from "./dialog/CreateUserDialog"; -import { UserDetailsDTO } from "../../../common/types"; +import { UserDetailsDTO } from "../../../../common/types"; /** * Types diff --git a/webapp/src/components/settings/Users/dialog/CreateUserDialog.tsx b/webapp/src/components/App/Settings/Users/dialog/CreateUserDialog.tsx similarity index 91% rename from webapp/src/components/settings/Users/dialog/CreateUserDialog.tsx rename to webapp/src/components/App/Settings/Users/dialog/CreateUserDialog.tsx index 264f9c088e..6f22da6aed 100644 --- a/webapp/src/components/settings/Users/dialog/CreateUserDialog.tsx +++ b/webapp/src/components/App/Settings/Users/dialog/CreateUserDialog.tsx @@ -8,10 +8,10 @@ import { RoleType, UserDetailsDTO, UserDTO, -} from "../../../../common/types"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; -import { createRole, createUser } from "../../../../services/api/user"; -import { SubmitHandlerData } from "../../../common/Form"; +} from "../../../../../common/types"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { createRole, createUser } from "../../../../../services/api/user"; +import { SubmitHandlerData } from "../../../../common/Form"; import UserFormDialog, { UserFormDialogProps } from "./UserFormDialog"; type InheritPropsToOmit = "title" | "titleIcon" | "onSubmit" | "onCancel"; diff --git a/webapp/src/components/settings/Users/dialog/UpdateUserDialog.tsx b/webapp/src/components/App/Settings/Users/dialog/UpdateUserDialog.tsx similarity index 90% rename from webapp/src/components/settings/Users/dialog/UpdateUserDialog.tsx rename to webapp/src/components/App/Settings/Users/dialog/UpdateUserDialog.tsx index 7b3ff0c996..9ffef0096c 100644 --- a/webapp/src/components/settings/Users/dialog/UpdateUserDialog.tsx +++ b/webapp/src/components/App/Settings/Users/dialog/UpdateUserDialog.tsx @@ -3,12 +3,16 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { usePromise as usePromiseWrapper } from "react-use"; import { useSnackbar } from "notistack"; -import { GroupDTO, RoleType, UserDetailsDTO } from "../../../../common/types"; -import { createRole, deleteUserRoles } from "../../../../services/api/user"; +import { + GroupDTO, + RoleType, + UserDetailsDTO, +} from "../../../../../common/types"; +import { createRole, deleteUserRoles } from "../../../../../services/api/user"; import UserFormDialog, { UserFormDialogProps } from "./UserFormDialog"; import { UserEdit } from ".."; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; -import { SubmitHandlerData } from "../../../common/Form"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { SubmitHandlerData } from "../../../../common/Form"; type InheritPropsToOmit = | "title" diff --git a/webapp/src/components/settings/Users/dialog/UserFormDialog/UserForm.tsx b/webapp/src/components/App/Settings/Users/dialog/UserFormDialog/UserForm.tsx similarity index 95% rename from webapp/src/components/settings/Users/dialog/UserFormDialog/UserForm.tsx rename to webapp/src/components/App/Settings/Users/dialog/UserFormDialog/UserForm.tsx index fb9319f507..f11e76b78b 100644 --- a/webapp/src/components/settings/Users/dialog/UserFormDialog/UserForm.tsx +++ b/webapp/src/components/App/Settings/Users/dialog/UserFormDialog/UserForm.tsx @@ -28,12 +28,12 @@ import { RESERVED_USER_NAMES, ROLE_TYPE_KEYS, } from "../../../utils"; -import { GroupDTO, RoleType } from "../../../../../common/types"; -import { roleToString, sortByName } from "../../../../../services/utils"; -import usePromise from "../../../../../hooks/usePromise"; -import { getGroups } from "../../../../../services/api/user"; +import { GroupDTO, RoleType } from "../../../../../../common/types"; +import { roleToString, sortByName } from "../../../../../../services/utils"; +import usePromise from "../../../../../../hooks/usePromise"; +import { getGroups } from "../../../../../../services/api/user"; import { UserFormDialogProps } from "."; -import { FormObj } from "../../../../common/Form"; +import { FormObj } from "../../../../../common/Form"; interface Props extends FormObj { onlyPermissions?: UserFormDialogProps["onlyPermissions"]; diff --git a/webapp/src/components/settings/Users/dialog/UserFormDialog/index.tsx b/webapp/src/components/App/Settings/Users/dialog/UserFormDialog/index.tsx similarity index 89% rename from webapp/src/components/settings/Users/dialog/UserFormDialog/index.tsx rename to webapp/src/components/App/Settings/Users/dialog/UserFormDialog/index.tsx index c4f3f19442..f3d0d915c2 100644 --- a/webapp/src/components/settings/Users/dialog/UserFormDialog/index.tsx +++ b/webapp/src/components/App/Settings/Users/dialog/UserFormDialog/index.tsx @@ -1,8 +1,8 @@ import { DialogContentText } from "@mui/material"; import FormDialog, { FormDialogProps, -} from "../../../../common/dialogs/FormDialog"; -import { GroupDTO, RoleType } from "../../../../../common/types"; +} from "../../../../../common/dialogs/FormDialog"; +import { GroupDTO, RoleType } from "../../../../../../common/types"; import UserForm from "./UserForm"; /** diff --git a/webapp/src/components/settings/Users/index.tsx b/webapp/src/components/App/Settings/Users/index.tsx similarity index 94% rename from webapp/src/components/settings/Users/index.tsx rename to webapp/src/components/App/Settings/Users/index.tsx index f20e864dac..39f667d66e 100644 --- a/webapp/src/components/settings/Users/index.tsx +++ b/webapp/src/components/App/Settings/Users/index.tsx @@ -20,15 +20,15 @@ import { usePromise as usePromiseWrapper, useUpdateEffect } from "react-use"; import { Action } from "redux"; import { useSnackbar } from "notistack"; import * as R from "ramda"; -import { deleteUser, getUsers } from "../../../services/api/user"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +import { deleteUser, getUsers } from "../../../../services/api/user"; +import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; import Header from "./Header"; import { RESERVED_USER_NAMES } from "../utils"; -import { UserDetailsDTO } from "../../../common/types"; +import { UserDetailsDTO } from "../../../../common/types"; import UpdateUserDialog from "./dialog/UpdateUserDialog"; -import { sortByName } from "../../../services/utils"; +import { sortByName } from "../../../../services/utils"; /** * Types diff --git a/webapp/src/pages/Settings.tsx b/webapp/src/components/App/Settings/index.tsx similarity index 84% rename from webapp/src/pages/Settings.tsx rename to webapp/src/components/App/Settings/index.tsx index a24e5c3940..d460de40e5 100644 --- a/webapp/src/pages/Settings.tsx +++ b/webapp/src/components/App/Settings/index.tsx @@ -3,13 +3,16 @@ import { TabContext, TabList, TabPanel } from "@mui/lab"; import { Box, Tab } from "@mui/material"; import { SyntheticEvent, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import RootPage from "../components/common/page/RootPage"; -import Groups from "../components/settings/Groups"; -import Maintenance from "../components/settings/Maintenance"; -import Tokens from "../components/settings/Tokens"; -import Users from "../components/settings/Users"; -import useAppSelector from "../redux/hooks/useAppSelector"; -import { isAuthUserAdmin, isAuthUserInGroupAdmin } from "../redux/selectors"; +import RootPage from "../../common/page/RootPage"; +import Groups from "./Groups"; +import Maintenance from "./Maintenance"; +import Tokens from "./Tokens"; +import Users from "./Users"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { + isAuthUserAdmin, + isAuthUserInGroupAdmin, +} from "../../../redux/selectors"; /** * Component diff --git a/webapp/src/components/settings/utils.ts b/webapp/src/components/App/Settings/utils.ts similarity index 82% rename from webapp/src/components/settings/utils.ts rename to webapp/src/components/App/Settings/utils.ts index a73a050848..8d0124a990 100644 --- a/webapp/src/components/settings/utils.ts +++ b/webapp/src/components/App/Settings/utils.ts @@ -1,5 +1,5 @@ import * as RA from "ramda-adjunct"; -import { RoleType } from "../../common/types"; +import { RoleType } from "../../../common/types"; export const RESERVED_USER_NAMES = ["admin"]; export const RESERVED_GROUP_NAMES = ["admin"]; diff --git a/webapp/src/components/singlestudy/Commands/Edition/AddCommandDialog.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/AddCommandDialog.tsx similarity index 97% rename from webapp/src/components/singlestudy/Commands/Edition/AddCommandDialog.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/AddCommandDialog.tsx index d54c01c1dd..49db2c6a6f 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/AddCommandDialog.tsx +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/AddCommandDialog.tsx @@ -1,7 +1,7 @@ import { Autocomplete, Box, Button, TextField } from "@mui/material"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import BasicDialog from "../../../common/dialogs/BasicDialog"; +import BasicDialog from "../../../../common/dialogs/BasicDialog"; import { CommandList } from "./utils"; interface PropTypes { diff --git a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandImportButton.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandImportButton.tsx similarity index 100% rename from webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandImportButton.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandImportButton.tsx diff --git a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx similarity index 98% rename from webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx index a287c2e2f5..84bea0fa9b 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx @@ -16,8 +16,8 @@ import { } from "@mui/material"; import { CommandItem } from "../../commandTypes"; import CommandImportButton from "../CommandImportButton"; -import { CommandResultDTO } from "../../../../../../common/types"; -import LogModal from "../../../../../common/LogModal"; +import { CommandResultDTO } from "../../../../../../../common/types"; +import LogModal from "../../../../../../common/LogModal"; import { detailsStyle, DraggableAccorderon, diff --git a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts similarity index 99% rename from webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts rename to webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts index 700dbd9778..55b979cdfc 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts @@ -1,6 +1,6 @@ import { Accordion, Box, styled } from "@mui/material"; import DeleteIcon from "@mui/icons-material/HighlightOff"; -import { PAPER_BACKGROUND_NO_TRANSPARENCY } from "../../../../../../theme"; +import { PAPER_BACKGROUND_NO_TRANSPARENCY } from "../../../../../../../theme"; export const ItemContainer = styled(Box, { shouldForwardProp: (prop) => prop !== "onTopVisible", diff --git a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx similarity index 100% rename from webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx diff --git a/webapp/src/components/singlestudy/Commands/Edition/commandTypes.ts b/webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts similarity index 97% rename from webapp/src/components/singlestudy/Commands/Edition/commandTypes.ts rename to webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts index 041a79e008..c933a939ac 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/commandTypes.ts +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts @@ -1,4 +1,4 @@ -import { CommandResultDTO } from "../../../../common/types"; +import { CommandResultDTO } from "../../../../../common/types"; /* eslint-disable camelcase */ export interface CommandItem { diff --git a/webapp/src/components/singlestudy/Commands/Edition/index.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/index.tsx similarity index 97% rename from webapp/src/components/singlestudy/Commands/Edition/index.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/index.tsx index f9b745d97b..294df8f5ac 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/index.tsx +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/index.tsx @@ -30,7 +30,7 @@ import { replaceCommands, applyCommands, getStudyTask, -} from "../../../../services/api/variant"; +} from "../../../../../services/api/variant"; import AddCommandDialog from "./AddCommandDialog"; import { CommandDTO, @@ -39,18 +39,18 @@ import { CommandResultDTO, TaskEventPayload, TaskStatus, -} from "../../../../common/types"; +} from "../../../../../common/types"; import CommandImportButton from "./DraggableCommands/CommandImportButton"; -import { getTask } from "../../../../services/api/tasks"; +import { getTask } from "../../../../../services/api/tasks"; import { Body, EditHeader, Header, headerIconStyle, Root } from "./style"; -import SimpleLoader from "../../../common/loaders/SimpleLoader"; -import NoContent from "../../../common/page/NoContent"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../common/loaders/SimpleLoader"; +import NoContent from "../../../../common/page/NoContent"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; import { addWsMessageListener, sendWsSubscribeMessage, WsChannel, -} from "../../../../services/webSockets"; +} from "../../../../../services/webSockets"; const logError = debug("antares:variantedition:error"); diff --git a/webapp/src/components/singlestudy/Commands/Edition/style.ts b/webapp/src/components/App/Singlestudy/Commands/Edition/style.ts similarity index 100% rename from webapp/src/components/singlestudy/Commands/Edition/style.ts rename to webapp/src/components/App/Singlestudy/Commands/Edition/style.ts diff --git a/webapp/src/components/singlestudy/Commands/Edition/utils.ts b/webapp/src/components/App/Singlestudy/Commands/Edition/utils.ts similarity index 98% rename from webapp/src/components/singlestudy/Commands/Edition/utils.ts rename to webapp/src/components/App/Singlestudy/Commands/Edition/utils.ts index fc2d6140e1..89b415ef62 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/utils.ts +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/utils.ts @@ -3,7 +3,7 @@ import { CommandResultDTO, TaskDTO, TaskStatus, -} from "../../../../common/types"; +} from "../../../../../common/types"; import { CommandEnum, CommandItem, JsonCommandItem } from "./commandTypes"; export const CommandList = [ diff --git a/webapp/src/components/singlestudy/Commands/index.tsx b/webapp/src/components/App/Singlestudy/Commands/index.tsx similarity index 100% rename from webapp/src/components/singlestudy/Commands/index.tsx rename to webapp/src/components/App/Singlestudy/Commands/index.tsx diff --git a/webapp/src/components/singlestudy/Commands/style.ts b/webapp/src/components/App/Singlestudy/Commands/style.ts similarity index 100% rename from webapp/src/components/singlestudy/Commands/style.ts rename to webapp/src/components/App/Singlestudy/Commands/style.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx similarity index 85% rename from webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx index 78dd2d3416..982d5a1737 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx @@ -4,12 +4,12 @@ import { useNavigate } from "react-router"; import { Button, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; -import SingleSelect from "../../../../common/SelectSingle"; -import { GenericInfo, VariantTree } from "../../../../../common/types"; -import { createVariant } from "../../../../../services/api/variant"; -import { createListFromTree } from "../../../../../services/utils"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import BasicDialog from "../../../../common/dialogs/BasicDialog"; +import SingleSelect from "../../../../../common/SelectSingle"; +import { GenericInfo, VariantTree } from "../../../../../../common/types"; +import { createVariant } from "../../../../../../services/api/variant"; +import { createListFromTree } from "../../../../../../services/utils"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import BasicDialog from "../../../../../common/dialogs/BasicDialog"; import { InputContainer, Root } from "./style"; interface Props { diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/style.ts b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/style.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/style.ts rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/style.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx similarity index 92% rename from webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx index 0262713501..1afbe47129 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx @@ -10,11 +10,11 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useSnackbar } from "notistack"; import { AxiosError } from "axios"; -import { JobStatus, LaunchJob } from "../../../../../common/types"; -import { convertUTCToLocalTime } from "../../../../../services/utils"; -import { killStudy } from "../../../../../services/api/study"; -import LaunchJobLogView from "../../../../tasks/LaunchJobLogView"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { JobStatus, LaunchJob } from "../../../../../../common/types"; +import { convertUTCToLocalTime } from "../../../../../../services/utils"; +import { killStudy } from "../../../../../../services/api/study"; +import LaunchJobLogView from "../../../../Tasks/LaunchJobLogView"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; import { CancelContainer, JobRoot, @@ -23,7 +23,7 @@ import { StepLabelRoot, StepLabelRow, } from "./style"; -import ConfirmationDialog from "../../../../common/dialogs/ConfirmationDialog"; +import ConfirmationDialog from "../../../../../common/dialogs/ConfirmationDialog"; export const ColorStatus = { running: "warning.main", diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/index.tsx similarity index 94% rename from webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/index.tsx index 02f026ac0e..734943fa30 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/index.tsx @@ -9,18 +9,18 @@ import { StudyMetadata, WSEvent, WSMessage, -} from "../../../../../common/types"; +} from "../../../../../../common/types"; import { getStudyJobs, mapLaunchJobDTO, -} from "../../../../../services/api/study"; +} from "../../../../../../services/api/study"; import { addWsMessageListener, sendWsSubscribeMessage, WsChannel, -} from "../../../../../services/webSockets"; +} from "../../../../../../services/webSockets"; import JobStepper from "./JobStepper"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; const TitleHeader = styled(Box)(({ theme }) => ({ display: "flex", diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/style.ts b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/style.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/style.ts rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/style.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx similarity index 98% rename from webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx index 0d2ddc25c7..1ee7c64da7 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered"; import { convertDraftJSToXML, convertXMLToDraftJS } from "../utils"; -import BasicDialog from "../../../../../common/dialogs/BasicDialog"; +import BasicDialog from "../../../../../../common/dialogs/BasicDialog"; import { EditorButton, EditorContainer, diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/style.ts b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/style.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/style.ts rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/style.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/index.tsx similarity index 95% rename from webapp/src/components/singlestudy/HomeView/InformationView/Notes/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/index.tsx index 27a76720ee..de1b4c1be8 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/index.tsx @@ -10,12 +10,12 @@ import { editComments, getComments, getStudySynthesis, -} from "../../../../../services/api/study"; +} from "../../../../../../services/api/study"; import { convertXMLToDraftJS } from "./utils"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; import NoteEditorModal from "./NodeEditorModal"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; const Root = styled(Box)(({ theme }) => ({ flex: "0 0 40%", diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/utils.ts b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/utils.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/InformationView/Notes/utils.ts rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/utils.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx similarity index 91% rename from webapp/src/components/singlestudy/HomeView/InformationView/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx index b865f4d8a1..f1fee86ec3 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx @@ -3,13 +3,13 @@ import { Paper, Button, Box, Divider } from "@mui/material"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; -import { StudyMetadata, VariantTree } from "../../../../common/types"; +import { StudyMetadata, VariantTree } from "../../../../../common/types"; import CreateVariantModal from "./CreateVariantModal"; import LauncherHistory from "./LauncherHistory"; import Notes from "./Notes"; -import LauncherModal from "../../../studies/LauncherDialog"; -import { copyStudy } from "../../../../services/api/study"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import LauncherDialog from "../../../Studies/LauncherDialog"; +import { copyStudy } from "../../../../../services/api/study"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; interface Props { // eslint-disable-next-line react/no-unused-prop-types @@ -120,7 +120,7 @@ function InformationView(props: Props) { /> )} {study && openLauncherModal && ( - setOpenLauncherModal(false)} diff --git a/webapp/src/components/singlestudy/HomeView/Split.css b/webapp/src/components/App/Singlestudy/HomeView/Split.css similarity index 100% rename from webapp/src/components/singlestudy/HomeView/Split.css rename to webapp/src/components/App/Singlestudy/HomeView/Split.css diff --git a/webapp/src/components/singlestudy/HomeView/StudyTreeView/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/index.tsx similarity index 98% rename from webapp/src/components/singlestudy/HomeView/StudyTreeView/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/index.tsx index 023b1753d3..af0cd02a51 100644 --- a/webapp/src/components/singlestudy/HomeView/StudyTreeView/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import * as React from "react"; import { Box, styled } from "@mui/material"; -import { StudyMetadata, VariantTree } from "../../../../common/types"; +import { StudyMetadata, VariantTree } from "../../../../../common/types"; import { StudyTree, getTreeNodes } from "./utils"; import { CIRCLE_RADIUS, diff --git a/webapp/src/components/singlestudy/HomeView/StudyTreeView/treeconfig.ts b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/treeconfig.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/StudyTreeView/treeconfig.ts rename to webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/treeconfig.ts diff --git a/webapp/src/components/singlestudy/HomeView/StudyTreeView/utils.ts b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/utils.ts similarity index 98% rename from webapp/src/components/singlestudy/HomeView/StudyTreeView/utils.ts rename to webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/utils.ts index 0dfabb3c59..dc1f4f9cf0 100644 --- a/webapp/src/components/singlestudy/HomeView/StudyTreeView/utils.ts +++ b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/utils.ts @@ -3,7 +3,7 @@ import { GenericInfo, StudyMetadata, VariantTree, -} from "../../../../common/types"; +} from "../../../../../common/types"; export interface StudyTree { name: string; diff --git a/webapp/src/components/singlestudy/HomeView/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/index.tsx similarity index 96% rename from webapp/src/components/singlestudy/HomeView/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/index.tsx index 3220e64c20..9e92b33e0e 100644 --- a/webapp/src/components/singlestudy/HomeView/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/index.tsx @@ -2,7 +2,7 @@ import { useNavigate } from "react-router-dom"; import { Box } from "@mui/material"; import Split from "react-split"; -import { StudyMetadata, VariantTree } from "../../../common/types"; +import { StudyMetadata, VariantTree } from "../../../../common/types"; import "./Split.css"; import StudyTreeView from "./StudyTreeView"; import InformationView from "./InformationView"; diff --git a/webapp/src/components/singlestudy/NavHeader.tsx b/webapp/src/components/App/Singlestudy/NavHeader.tsx similarity index 95% rename from webapp/src/components/singlestudy/NavHeader.tsx rename to webapp/src/components/App/Singlestudy/NavHeader.tsx index 97afb0e7aa..62f8a3a270 100644 --- a/webapp/src/components/singlestudy/NavHeader.tsx +++ b/webapp/src/components/App/Singlestudy/NavHeader.tsx @@ -34,28 +34,28 @@ import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import { useTranslation } from "react-i18next"; import { indigo } from "@mui/material/colors"; -import { StudyMetadata, VariantTree } from "../../common/types"; -import { STUDIES_HEIGHT_HEADER } from "../../theme"; +import { StudyMetadata, VariantTree } from "../../../common/types"; +import { STUDIES_HEIGHT_HEADER } from "../../../theme"; import { archiveStudy as callArchiveStudy, unarchiveStudy as callUnarchiveStudy, -} from "../../services/api/study"; -import { deleteStudy, toggleFavorite } from "../../redux/ducks/studies"; -import LauncherDialog from "../studies/LauncherDialog"; +} from "../../../services/api/study"; +import { deleteStudy, toggleFavorite } from "../../../redux/ducks/studies"; +import LauncherDialog from "../Studies/LauncherDialog"; import PropertiesDialog from "./PropertiesDialog"; import { buildModificationDate, convertUTCToLocalTime, countAllChildrens, displayVersionName, -} from "../../services/utils"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import { isCurrentStudyFavorite } from "../../redux/selectors"; -import ExportDialog from "../studies/ExportModal"; -import StarToggle from "../common/StarToggle"; -import ConfirmationDialog from "../common/dialogs/ConfirmationDialog"; -import useAppSelector from "../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; +} from "../../../services/utils"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import { isCurrentStudyFavorite } from "../../../redux/selectors"; +import ExportDialog from "../Studies/ExportModal"; +import StarToggle from "../../common/StarToggle"; +import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; const logError = debug("antares:singlestudy:navheader:error"); diff --git a/webapp/src/components/singlestudy/PropertiesDialog/index.tsx b/webapp/src/components/App/Singlestudy/PropertiesDialog/index.tsx similarity index 93% rename from webapp/src/components/singlestudy/PropertiesDialog/index.tsx rename to webapp/src/components/App/Singlestudy/PropertiesDialog/index.tsx index 6e42f7ef18..18d35850d2 100644 --- a/webapp/src/components/singlestudy/PropertiesDialog/index.tsx +++ b/webapp/src/components/App/Singlestudy/PropertiesDialog/index.tsx @@ -5,26 +5,26 @@ import { Button, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; import { useSnackbar } from "notistack"; -import SingleSelect from "../../common/SelectSingle"; -import MultiSelect from "../../common/SelectMulti"; +import SingleSelect from "../../../common/SelectSingle"; +import MultiSelect from "../../../common/SelectMulti"; import { GenericInfo, GroupDTO, StudyMetadata, StudyMetadataPatchDTO, StudyPublicMode, -} from "../../../common/types"; -import TextSeparator from "../../common/TextSeparator"; +} from "../../../../common/types"; +import TextSeparator from "../../../common/TextSeparator"; import { addStudyGroup, changePublicMode, deleteStudyGroup, updateStudyMetadata, -} from "../../../services/api/study"; -import { getGroups } from "../../../services/api/user"; -import TagTextInput from "../../common/TagTextInput"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import BasicDialog from "../../common/dialogs/BasicDialog"; +} from "../../../../services/api/study"; +import { getGroups } from "../../../../services/api/user"; +import TagTextInput from "../../../common/TagTextInput"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import BasicDialog from "../../../common/dialogs/BasicDialog"; import { ElementContainer, InputElement, Root } from "./style"; const logErr = debug("antares:createstudyform:error"); diff --git a/webapp/src/components/singlestudy/PropertiesDialog/style.ts b/webapp/src/components/App/Singlestudy/PropertiesDialog/style.ts similarity index 100% rename from webapp/src/components/singlestudy/PropertiesDialog/style.ts rename to webapp/src/components/App/Singlestudy/PropertiesDialog/style.ts diff --git a/webapp/src/components/singlestudy/explore/Configuration/AdvancedParameters/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/AdvancedParameters/index.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Configuration/AdvancedParameters/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/AdvancedParameters/index.tsx index b30e280a21..e483e81600 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/AdvancedParameters/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/AdvancedParameters/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; function AdvancedParameters() { return ; diff --git a/webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx similarity index 94% rename from webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx index fecb2257b5..da7ae7ad46 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx @@ -3,19 +3,19 @@ import { Box, Divider, TextField } from "@mui/material"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { StyledFieldset } from "../styles"; -import SelectFE from "../../../../../common/fieldEditors/SelectFE"; -import { StudyMetadata } from "../../../../../../common/types"; -import { editStudy } from "../../../../../../services/api/study"; -import SwitchFE from "../../../../../common/fieldEditors/SwitchFE"; +import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; +import { StudyMetadata } from "../../../../../../../common/types"; +import { editStudy } from "../../../../../../../services/api/study"; +import SwitchFE from "../../../../../../common/fieldEditors/SwitchFE"; import { FIRST_JANUARY_OPTIONS, FormValues, WEEK_OPTIONS, YEAR_OPTIONS, } from "../utils"; -import BooleanFE from "../../../../../common/fieldEditors/BooleanFE"; -import { useFormContext } from "../../../../../common/Form"; -import useDebouncedEffect from "../../../../../../hooks/useDebouncedEffect"; +import BooleanFE from "../../../../../../common/fieldEditors/BooleanFE"; +import { useFormContext } from "../../../../../../common/Form"; +import useDebouncedEffect from "../../../../../../../hooks/useDebouncedEffect"; interface Props { study: StudyMetadata; diff --git a/webapp/src/components/singlestudy/explore/Configuration/General/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx similarity index 71% rename from webapp/src/components/singlestudy/explore/Configuration/General/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx index 4ad78a4069..504ba05a26 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/General/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx @@ -1,12 +1,12 @@ import { useOutletContext } from "react-router"; import * as R from "ramda"; -import { StudyMetadata } from "../../../../../common/types"; -import usePromiseWithSnackbarError from "../../../../../hooks/usePromiseWithSnackbarError"; +import { StudyMetadata } from "../../../../../../common/types"; +import usePromiseWithSnackbarError from "../../../../../../hooks/usePromiseWithSnackbarError"; import { getFormValues } from "./utils"; -import { PromiseStatus } from "../../../../../hooks/usePromise"; -import Form from "../../../../common/Form"; +import { PromiseStatus } from "../../../../../../hooks/usePromise"; +import Form from "../../../../../common/Form"; import Fields from "./Fields"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; function GeneralParameters() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Configuration/General/styles.ts b/webapp/src/components/App/Singlestudy/explore/Configuration/General/styles.ts similarity index 84% rename from webapp/src/components/singlestudy/explore/Configuration/General/styles.ts rename to webapp/src/components/App/Singlestudy/explore/Configuration/General/styles.ts index 7a598c31e8..47e21d4fb3 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/General/styles.ts +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/styles.ts @@ -1,5 +1,5 @@ import { styled, experimental_sx as sx } from "@mui/material"; -import Fieldset from "../../../../common/Fieldset"; +import Fieldset from "../../../../../common/Fieldset"; export const StyledFieldset = styled(Fieldset)( sx({ diff --git a/webapp/src/components/singlestudy/explore/Configuration/General/utils.ts b/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts similarity index 97% rename from webapp/src/components/singlestudy/explore/Configuration/General/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts index fb5c104b17..e99b91e072 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/General/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts @@ -1,6 +1,6 @@ import * as RA from "ramda-adjunct"; -import { StudyMetadata } from "../../../../../common/types"; -import { getStudyData } from "../../../../../services/api/study"; +import { StudyMetadata } from "../../../../../../common/types"; +import { getStudyData } from "../../../../../../services/api/study"; enum Month { January = "january", diff --git a/webapp/src/components/singlestudy/explore/Configuration/OptimizationPreferences/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/OptimizationPreferences/index.tsx similarity index 59% rename from webapp/src/components/singlestudy/explore/Configuration/OptimizationPreferences/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/OptimizationPreferences/index.tsx index 597bfb4e4c..dab08eabe8 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/OptimizationPreferences/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/OptimizationPreferences/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; function OptimizationPreferences() { return ; diff --git a/webapp/src/components/singlestudy/explore/Configuration/RegionalDistricts/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/RegionalDistricts/index.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Configuration/RegionalDistricts/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/RegionalDistricts/index.tsx index 310deea2ff..025121ed72 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/RegionalDistricts/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/RegionalDistricts/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; function RegionalDistricts() { return ; diff --git a/webapp/src/components/singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx similarity index 57% rename from webapp/src/components/singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx index c2715247a6..2957cbc159 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; function TimeSeriesManagement() { return ; diff --git a/webapp/src/components/singlestudy/explore/Configuration/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/index.tsx similarity index 92% rename from webapp/src/components/singlestudy/explore/Configuration/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/index.tsx index cc6eb882d2..ff552b1fce 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/index.tsx @@ -2,8 +2,8 @@ import { Paper } from "@mui/material"; import * as R from "ramda"; import { useMemo, useState } from "react"; -import PropertiesView from "../../../common/PropertiesView"; -import SplitLayoutView from "../../../common/SplitLayoutView"; +import PropertiesView from "../../../../common/PropertiesView"; +import SplitLayoutView from "../../../../common/SplitLayoutView"; import ListElement from "../common/ListElement"; import AdvancedParameters from "./AdvancedParameters"; import General from "./General"; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/AreaPropsView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreaPropsView.tsx similarity index 82% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/AreaPropsView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreaPropsView.tsx index 645ac340c8..b7a7953061 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/AreaPropsView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreaPropsView.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; -import { Area } from "../../../../../common/types"; -import PropertiesView from "../../../../common/PropertiesView"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getStudyAreas } from "../../../../../redux/selectors"; +import { Area } from "../../../../../../common/types"; +import PropertiesView from "../../../../../common/PropertiesView"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getStudyAreas } from "../../../../../../redux/selectors"; import ListElement from "../../common/ListElement"; -import { transformNameToId } from "../../../../../services/utils"; +import { transformNameToId } from "../../../../../../services/utils"; interface PropsType { studyId: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/AreasTab.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreasTab.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/AreasTab.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreasTab.tsx index 79d6a3f402..530a2792e8 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/AreasTab.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreasTab.tsx @@ -3,7 +3,7 @@ import { useMemo } from "react"; import { useOutletContext } from "react-router-dom"; import { Paper } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; import TabWrapper from "../../TabWrapper"; interface Props { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Hydro/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx similarity index 65% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Hydro/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx index b6da11eef8..030ffe91cc 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Hydro/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../../common/page/UnderConstruction"; import previewImage from "../Thermal/preview.png"; function Hydro() { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Load.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Load.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx index 392e722a22..4ce7112f71 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Load.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function Load() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/MiscGen.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx similarity index 66% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/MiscGen.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx index 6dbdb9106f..98fe2ca559 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/MiscGen.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function MiscGen() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx similarity index 93% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx index ca288efdce..74e8d27b8a 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx @@ -2,15 +2,15 @@ import { Box, TextField, Typography } from "@mui/material"; import { AxiosError } from "axios"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { editStudy } from "../../../../../../services/api/study"; -import SelectFE from "../../../../../common/fieldEditors/SelectFE"; -import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; -import Fieldset from "../../../../../common/Fieldset"; -import { FormObj } from "../../../../../common/Form"; -import ColorPicker from "../../../../../common/fieldEditors/ColorPickerFE"; -import { stringToRGB } from "../../../../../common/fieldEditors/ColorPickerFE/utils"; +import { editStudy } from "../../../../../../../services/api/study"; +import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; +import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; +import Fieldset from "../../../../../../common/Fieldset"; +import { FormObj } from "../../../../../../common/Form"; +import ColorPicker from "../../../../../../common/fieldEditors/ColorPickerFE"; +import { stringToRGB } from "../../../../../../common/fieldEditors/ColorPickerFE/utils"; import { getPropertiesPath, PropertiesFields } from "./utils"; -import SwitchFE from "../../../../../common/fieldEditors/SwitchFE"; +import SwitchFE from "../../../../../../common/fieldEditors/SwitchFE"; export default function PropertiesForm( props: FormObj & { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/common.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/common.ts similarity index 87% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/common.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/common.ts index a71ef6437f..31d0adbc33 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/common.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/common.ts @@ -1,6 +1,8 @@ import { result } from "lodash"; -import usePromise, { PromiseStatus } from "../../../../../../hooks/usePromise"; -import { getStudyData } from "../../../../../../services/api/study"; +import usePromise, { + PromiseStatus, +} from "../../../../../../../hooks/usePromise"; +import { getStudyData } from "../../../../../../../services/api/study"; export interface FieldElement { path?: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/index.tsx similarity index 74% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/index.tsx index a6ea7dc419..bdddc0a461 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/index.tsx @@ -2,14 +2,16 @@ import * as R from "ramda"; import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useOutletContext } from "react-router"; -import { StudyMetadata } from "../../../../../../common/types"; -import usePromise, { PromiseStatus } from "../../../../../../hooks/usePromise"; -import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import Form from "../../../../../common/Form"; +import { StudyMetadata } from "../../../../../../../common/types"; +import usePromise, { + PromiseStatus, +} from "../../../../../../../hooks/usePromise"; +import useAppSelector from "../../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../../redux/selectors"; +import Form from "../../../../../../common/Form"; import PropertiesForm from "./PropertiesForm"; import { getDefaultValues, PropertiesFields } from "./utils"; -import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; function Properties() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/utils.ts similarity index 95% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/utils.ts index 5586fe7018..7789f4a76f 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/utils.ts @@ -1,7 +1,7 @@ import { FieldValues } from "react-hook-form"; import { TFunction } from "react-i18next"; -import { getStudyData } from "../../../../../../services/api/study"; -import { RGBToString } from "../../../../../common/fieldEditors/ColorPickerFE/utils"; +import { getStudyData } from "../../../../../../../services/api/study"; +import { RGBToString } from "../../../../../../common/fieldEditors/ColorPickerFE/utils"; export interface PropertiesType { ui: { diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/index.tsx new file mode 100644 index 0000000000..f2307f8ff0 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/index.tsx @@ -0,0 +1,8 @@ +import UnderConstruction from "../../../../../../common/page/UnderConstruction"; +import previewImage from "./preview.png"; + +function Renewables() { + return ; +} + +export default Renewables; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/preview.png b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/preview.png similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/preview.png rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/preview.png diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Reserve.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx similarity index 64% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Reserve.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx index f0d6ba1121..74c319e540 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Reserve.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function Reserve() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Solar.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Solar.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx index da25df93dc..debfe9f6cd 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Solar.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function Solar() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/index.tsx similarity index 65% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/index.tsx index 024ee1310b..12417d1c0c 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../../common/page/UnderConstruction"; import previewImage from "./preview.png"; function Thermal() { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/preview.png b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/preview.png similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/preview.png rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/preview.png diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Wind.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Wind.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx index ad7368293d..6e82215308 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Wind.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function Wind() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/index.tsx similarity index 81% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/index.tsx index c7448e08f2..2d8b3fdeb1 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/index.tsx @@ -2,17 +2,17 @@ import { Box } from "@mui/material"; import * as R from "ramda"; import { ReactNode } from "react"; import { useOutletContext } from "react-router"; -import { StudyMetadata } from "../../../../../common/types"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import NoContent from "../../../../common/page/NoContent"; -import SplitLayoutView from "../../../../common/SplitLayoutView"; +import { StudyMetadata } from "../../../../../../common/types"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import NoContent from "../../../../../common/page/NoContent"; +import SplitLayoutView from "../../../../../common/SplitLayoutView"; import AreaPropsView from "./AreaPropsView"; import AreasTab from "./AreasTab"; import useStudyData from "../../hooks/useStudyData"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../../../../redux/hooks/useAppDispatch"; -import { setCurrentArea } from "../../../../../redux/ducks/studyDataSynthesis"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../../../../redux/hooks/useAppDispatch"; +import { setCurrentArea } from "../../../../../../redux/ducks/studyDataSynthesis"; function Areas() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/index.tsx similarity index 73% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/index.tsx index c107e441ba..d77e5c656f 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/index.tsx @@ -1,8 +1,8 @@ import UnderConstruction from "../../../../../common/page/UnderConstruction"; import previewImage from "./preview.png"; -function Renewables() { +function BindingConstraint() { return ; } -export default Renewables; +export default BindingConstraint; diff --git a/webapp/src/components/singlestudy/explore/Modelization/BindingConstraints/preview.png b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/preview.png similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/BindingConstraints/preview.png rename to webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/preview.png diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx similarity index 90% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx index ea5467b227..b01dca1d75 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx @@ -6,11 +6,14 @@ import { useSnackbar } from "notistack"; import { useTranslation } from "react-i18next"; import { Box, Button } from "@mui/material"; import GetAppOutlinedIcon from "@mui/icons-material/GetAppOutlined"; -import { getStudyData, importFile } from "../../../../../../services/api/study"; +import { + getStudyData, + importFile, +} from "../../../../../../../services/api/study"; import { Header, Root, Content } from "./style"; -import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; -import ImportDialog from "../../../../../common/dialogs/ImportDialog"; +import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; +import ImportDialog from "../../../../../../common/dialogs/ImportDialog"; const logErr = debug("antares:createimportform:error"); diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx similarity index 92% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx index 1ce1ca3d70..37f4ac97e8 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx @@ -6,10 +6,13 @@ import { useTranslation } from "react-i18next"; import ReactJson from "react-json-view"; import SaveIcon from "@mui/icons-material/Save"; import { Box, Button, Typography } from "@mui/material"; -import { editStudy, getStudyData } from "../../../../../../services/api/study"; -import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import { + editStudy, + getStudyData, +} from "../../../../../../../services/api/study"; +import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; import { Header, Root, Content } from "./style"; -import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; interface PropTypes { data: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx similarity index 90% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx index e988667ea5..90b8120530 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx @@ -10,20 +10,23 @@ import GetAppOutlinedIcon from "@mui/icons-material/GetAppOutlined"; import { getStudyData, importFile, -} from "../../../../../../../services/api/study"; -import { MatrixType, MatrixEditDTO } from "../../../../../../../common/types"; +} from "../../../../../../../../services/api/study"; +import { + MatrixType, + MatrixEditDTO, +} from "../../../../../../../../common/types"; import { Header, Root, Content } from "../style"; -import usePromiseWithSnackbarError from "../../../../../../../hooks/usePromiseWithSnackbarError"; +import usePromiseWithSnackbarError from "../../../../../../../../hooks/usePromiseWithSnackbarError"; import { StyledButton } from "./style"; -import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; -import NoContent from "../../../../../../common/page/NoContent"; -import ImportDialog from "../../../../../../common/dialogs/ImportDialog"; -import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; -import EditableMatrix from "../../../../../../common/EditableMatrix"; +import useEnqueueErrorSnackbar from "../../../../../../../../hooks/useEnqueueErrorSnackbar"; +import NoContent from "../../../../../../../common/page/NoContent"; +import ImportDialog from "../../../../../../../common/dialogs/ImportDialog"; +import SimpleLoader from "../../../../../../../common/loaders/SimpleLoader"; +import EditableMatrix from "../../../../../../../common/EditableMatrix"; import { editMatrix, getStudyMatrixIndex, -} from "../../../../../../../services/api/matrix"; +} from "../../../../../../../../services/api/matrix"; const logErr = debug("antares:createimportform:error"); diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/style.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/style.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/style.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/style.ts diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx similarity index 95% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx index adadb250c0..8cc91350af 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx @@ -4,7 +4,7 @@ import { Box } from "@mui/material"; import StudyFileView from "./StudyFileView"; import StudyJsonView from "./StudyJsonView"; import StudyMatrixView from "./StudyMatrixView/StudyMatrixView"; -import { StudyDataType } from "../../../../../../common/types"; +import { StudyDataType } from "../../../../../../../common/types"; interface PropTypes { study: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/style.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/style.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/style.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/style.ts diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/utils/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/utils/utils.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/utils/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/utils/utils.ts diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx index 65adba32ae..fc7e2157e5 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx @@ -4,7 +4,7 @@ import { Box } from "@mui/material"; import { TreeItem, TreeView } from "@mui/lab"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; -import { StudyDataType } from "../../../../../../common/types"; +import { StudyDataType } from "../../../../../../../common/types"; import { getStudyParams } from "./utils"; interface ItemPropTypes { diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts similarity index 93% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts index 4d8e1f8520..ea0b7d6cb5 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts @@ -1,7 +1,7 @@ import IntegrationInstructionsIcon from "@mui/icons-material/IntegrationInstructions"; import TextSnippetIcon from "@mui/icons-material/TextSnippet"; import { SvgIconComponent } from "@mui/icons-material"; -import { StudyDataType } from "../../../../../../common/types"; +import { StudyDataType } from "../../../../../../../common/types"; export interface StudyParams { type: StudyDataType; diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx similarity index 89% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx index 5d1b32fab7..d07dc91403 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx @@ -4,12 +4,12 @@ import debug from "debug"; import { useTranslation } from "react-i18next"; import { useOutletContext } from "react-router-dom"; import { Box } from "@mui/material"; -import { getStudyData } from "../../../../../services/api/study"; +import { getStudyData } from "../../../../../../services/api/study"; import StudyTreeView from "./StudyTreeView"; import StudyDataView from "./StudyDataView"; -import { StudyDataType, StudyMetadata } from "../../../../../common/types"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; +import { StudyDataType, StudyMetadata } from "../../../../../../common/types"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; const logError = debug("antares:studyview:error"); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkPropsView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkPropsView.tsx similarity index 84% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkPropsView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkPropsView.tsx index 6f5c502e8c..fe3b605fe7 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkPropsView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkPropsView.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; -import PropertiesView from "../../../../common/PropertiesView"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getStudyLinks } from "../../../../../redux/selectors"; +import PropertiesView from "../../../../../common/PropertiesView"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getStudyLinks } from "../../../../../../redux/selectors"; import ListElement from "../../common/ListElement"; -import { LinkElement } from "../../../../../common/types"; +import { LinkElement } from "../../../../../../common/types"; interface PropsType { studyId: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx similarity index 93% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx index 58fcd7b9cc..14ac6a533b 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx @@ -2,19 +2,19 @@ import { Box } from "@mui/material"; import { AxiosError } from "axios"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { editStudy } from "../../../../../../services/api/study"; -import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; -import Fieldset from "../../../../../common/Fieldset"; -import { AutoSubmitHandler, FormObj } from "../../../../../common/Form"; +import { editStudy } from "../../../../../../../services/api/study"; +import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; +import Fieldset from "../../../../../../common/Fieldset"; +import { AutoSubmitHandler, FormObj } from "../../../../../../common/Form"; import { getLinkPath, LinkFields } from "./utils"; -import SwitchFE from "../../../../../common/fieldEditors/SwitchFE"; +import SwitchFE from "../../../../../../common/fieldEditors/SwitchFE"; import { LinkElement, MatrixStats, StudyMetadata, -} from "../../../../../../common/types"; -import SelectFE from "../../../../../common/fieldEditors/SelectFE"; -import MatrixInput from "../../../../../common/MatrixInput"; +} from "../../../../../../../common/types"; +import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; +import MatrixInput from "../../../../../../common/MatrixInput"; import LinkMatrixView from "./LinkMatrixView"; export default function LinkForm( diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx similarity index 95% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx index e8b4bea0bf..429092c5d9 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx @@ -5,8 +5,8 @@ import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; import { useTranslation } from "react-i18next"; -import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; -import MatrixInput from "../../../../../common/MatrixInput"; +import { MatrixStats, StudyMetadata } from "../../../../../../../common/types"; +import MatrixInput from "../../../../../../common/MatrixInput"; export const StyledTab = styled(Tabs)({ width: "98%", diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/index.tsx similarity index 80% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/index.tsx index 3f02ad24d3..f180bf152f 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/index.tsx @@ -1,12 +1,14 @@ import * as R from "ramda"; import { Box } from "@mui/material"; import { useOutletContext } from "react-router"; -import { LinkElement, StudyMetadata } from "../../../../../../common/types"; -import usePromise, { PromiseStatus } from "../../../../../../hooks/usePromise"; -import Form from "../../../../../common/Form"; +import { LinkElement, StudyMetadata } from "../../../../../../../common/types"; +import usePromise, { + PromiseStatus, +} from "../../../../../../../hooks/usePromise"; +import Form from "../../../../../../common/Form"; import LinkForm from "./LinkForm"; import { getDefaultValues, LinkFields } from "./utils"; -import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; interface Props { link: LinkElement; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/utils.ts similarity index 97% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/utils.ts index e0d887bc93..ee6458fced 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/utils.ts @@ -1,5 +1,5 @@ import { FieldValues } from "react-hook-form"; -import { getStudyData } from "../../../../../../services/api/study"; +import { getStudyData } from "../../../../../../../services/api/study"; type TransCapacitiesType = "infinite" | "ignore" | "enabled"; type AssetType = "ac" | "dc" | "gaz" | "virt"; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/index.tsx similarity index 80% rename from webapp/src/components/singlestudy/explore/Modelization/Links/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/index.tsx index f5ecb359ef..f53f711817 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/index.tsx @@ -2,16 +2,19 @@ import { Box } from "@mui/material"; import * as R from "ramda"; import { ReactNode } from "react"; import { useOutletContext } from "react-router"; -import { StudyMetadata } from "../../../../../common/types"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import NoContent from "../../../../common/page/NoContent"; -import SplitLayoutView from "../../../../common/SplitLayoutView"; +import { StudyMetadata } from "../../../../../../common/types"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import NoContent from "../../../../../common/page/NoContent"; +import SplitLayoutView from "../../../../../common/SplitLayoutView"; import LinkPropsView from "./LinkPropsView"; import useStudyData from "../../hooks/useStudyData"; -import { getCurrentLinkId, selectLinks } from "../../../../../redux/selectors"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../../../../redux/hooks/useAppDispatch"; -import { setCurrentLink } from "../../../../../redux/ducks/studyDataSynthesis"; +import { + getCurrentLinkId, + selectLinks, +} from "../../../../../../redux/selectors"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../../../../redux/hooks/useAppDispatch"; +import { setCurrentLink } from "../../../../../../redux/ducks/studyDataSynthesis"; import LinkView from "./LinkView"; function Links() { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/CreateAreaModal.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/CreateAreaModal.tsx similarity index 93% rename from webapp/src/components/singlestudy/explore/Modelization/Map/CreateAreaModal.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/CreateAreaModal.tsx index 6407f528ae..5292da842a 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/CreateAreaModal.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/CreateAreaModal.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { Box, Button, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useSnackbar } from "notistack"; -import { isStringEmpty } from "../../../../../services/utils"; -import BasicDialog from "../../../../common/dialogs/BasicDialog"; +import { isStringEmpty } from "../../../../../../services/utils"; +import BasicDialog from "../../../../../common/dialogs/BasicDialog"; interface PropType { open: boolean; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/GraphView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/GraphView.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Modelization/Map/GraphView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/GraphView.tsx index 8a03febe72..a811b33490 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/GraphView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/GraphView.tsx @@ -1,6 +1,6 @@ import { RefObject } from "react"; import { Graph, GraphLink, GraphNode } from "react-d3-graph"; -import { LinkProperties, NodeProperties } from "../../../../../common/types"; +import { LinkProperties, NodeProperties } from "../../../../../../common/types"; import NodeView from "./NodeView"; interface GraphViewProps { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/LinksView.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/LinksView.tsx index 44920b71e4..c34a4ed988 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/LinksView.tsx @@ -3,7 +3,7 @@ import { ListItemText, ListItem, Typography, Box, styled } from "@mui/material"; import { useTranslation } from "react-i18next"; import AutoSizer from "react-virtualized-auto-sizer"; import { areEqual, FixedSizeList, ListChildComponentProps } from "react-window"; -import { LinkProperties, NodeProperties } from "../../../../../common/types"; +import { LinkProperties, NodeProperties } from "../../../../../../common/types"; const ROW_ITEM_SIZE = 40; const BUTTONS_SIZE = 40; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/MapPropsView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/MapPropsView.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Modelization/Map/MapPropsView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/MapPropsView.tsx index 42246561f7..17f80fdfdd 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/MapPropsView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/MapPropsView.tsx @@ -5,9 +5,9 @@ import { NodeProperties, UpdateAreaUi, isNode, -} from "../../../../../common/types"; +} from "../../../../../../common/types"; import PanelView from "./PanelView"; -import PropertiesView from "../../../../common/PropertiesView"; +import PropertiesView from "../../../../../common/PropertiesView"; import ListElement from "../../common/ListElement"; interface PropsType { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/NodeView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/NodeView.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Modelization/Map/NodeView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/NodeView.tsx index 719c453d51..a90de7f922 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/NodeView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/NodeView.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef } from "react"; import { Box, styled } from "@mui/material"; import AddLinkIcon from "@mui/icons-material/AddLink"; -import { NodeProperties } from "../../../../../common/types"; -import { rgbToHsl } from "../../../../../services/utils"; +import { NodeProperties } from "../../../../../../common/types"; +import { rgbToHsl } from "../../../../../../services/utils"; const nodeStyle = { opacity: ".9", diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/PanelView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/PanelView.tsx similarity index 98% rename from webapp/src/components/singlestudy/explore/Modelization/Map/PanelView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/PanelView.tsx index cae6a2d3dd..114ea1b3ad 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/PanelView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/PanelView.tsx @@ -14,9 +14,9 @@ import { NodeProperties, LinkProperties, UpdateAreaUi, -} from "../../../../../common/types"; +} from "../../../../../../common/types"; import LinksView from "./LinksView"; -import ConfirmationDialog from "../../../../common/dialogs/ConfirmationDialog"; +import ConfirmationDialog from "../../../../../common/dialogs/ConfirmationDialog"; export const StyledDeleteIcon = styled(DeleteIcon)(({ theme }) => ({ cursor: "pointer", diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx similarity index 95% rename from webapp/src/components/singlestudy/explore/Modelization/Map/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx index c750c4949f..c4532cd5eb 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx @@ -12,27 +12,27 @@ import { NodeProperties, StudyMetadata, UpdateAreaUi, -} from "../../../../../common/types"; -import SplitLayoutView from "../../../../common/SplitLayoutView"; +} from "../../../../../../common/types"; +import SplitLayoutView from "../../../../../common/SplitLayoutView"; import { createArea, updateAreaUI, deleteArea, deleteLink, createLink, -} from "../../../../../services/api/studydata"; +} from "../../../../../../services/api/studydata"; import { getAreaPositions, getStudySynthesis, -} from "../../../../../services/api/study"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; +} from "../../../../../../services/api/study"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; import GraphView from "./GraphView"; import MapPropsView from "./MapPropsView"; import CreateAreaModal from "./CreateAreaModal"; -import mapbackground from "../../../../../assets/mapbackground.png"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import { setCurrentArea } from "../../../../../redux/ducks/studyDataSynthesis"; -import useAppDispatch from "../../../../../redux/hooks/useAppDispatch"; +import mapbackground from "./mapbackground.png"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import { setCurrentArea } from "../../../../../../redux/ducks/studyDataSynthesis"; +import useAppDispatch from "../../../../../../redux/hooks/useAppDispatch"; const FONT_SIZE = 16; const NODE_HEIGHT = 400; diff --git a/webapp/src/assets/mapbackground.png b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/mapbackground.png similarity index 100% rename from webapp/src/assets/mapbackground.png rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/mapbackground.png diff --git a/webapp/src/components/singlestudy/explore/Modelization/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/index.tsx similarity index 95% rename from webapp/src/components/singlestudy/explore/Modelization/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/index.tsx index 288ff7bc3e..1fcdc0045f 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/index.tsx @@ -3,7 +3,7 @@ import { useMemo } from "react"; import { useOutletContext } from "react-router-dom"; import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { StudyMetadata } from "../../../../common/types"; +import { StudyMetadata } from "../../../../../common/types"; import TabWrapper from "../TabWrapper"; function Modelization() { diff --git a/webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx similarity index 87% rename from webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index 4bf987e2f6..850b450ff9 100644 --- a/webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -2,9 +2,9 @@ import { useNavigate, useOutletContext } from "react-router"; import { useTranslation } from "react-i18next"; import { Box, Button } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; import previewImage from "./preview.png"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; function ResultDetails() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Results/ResultDetails/preview.png b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/preview.png similarity index 100% rename from webapp/src/components/singlestudy/explore/Results/ResultDetails/preview.png rename to webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/preview.png diff --git a/webapp/src/components/singlestudy/explore/Results/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/index.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/Results/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Results/index.tsx index 1e6edee5f3..8ee68f2238 100644 --- a/webapp/src/components/singlestudy/explore/Results/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/index.tsx @@ -21,19 +21,19 @@ import * as R from "ramda"; import { useNavigate, useOutletContext } from "react-router-dom"; import { grey } from "@mui/material/colors"; import moment from "moment"; -import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; +import usePromiseWithSnackbarError from "../../../../../hooks/usePromiseWithSnackbarError"; import { downloadJobOutput, getStudyJobs, getStudyOutputs, -} from "../../../../services/api/study"; +} from "../../../../../services/api/study"; import { LaunchJob, StudyMetadata, StudyOutput, -} from "../../../../common/types"; -import { convertUTCToLocalTime } from "../../../../services/utils"; -import LaunchJobLogView from "../../../tasks/LaunchJobLogView"; +} from "../../../../../common/types"; +import { convertUTCToLocalTime } from "../../../../../services/utils"; +import LaunchJobLogView from "../../../Tasks/LaunchJobLogView"; interface OutputDetail { name: string; diff --git a/webapp/src/components/singlestudy/explore/TabWrapper.tsx b/webapp/src/components/App/Singlestudy/explore/TabWrapper.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/TabWrapper.tsx rename to webapp/src/components/App/Singlestudy/explore/TabWrapper.tsx index ed41c8e49c..d8b43793ba 100644 --- a/webapp/src/components/singlestudy/explore/TabWrapper.tsx +++ b/webapp/src/components/App/Singlestudy/explore/TabWrapper.tsx @@ -6,7 +6,7 @@ import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; -import { StudyMetadata } from "../../../common/types"; +import { StudyMetadata } from "../../../../common/types"; export const StyledTab = styled(Tabs, { shouldForwardProp: (prop) => prop !== "border" && prop !== "tabStyle", diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx index 8fcaf98927..a0b86080ab 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx @@ -11,7 +11,7 @@ import { import { useTranslation } from "react-i18next"; import SaveIcon from "@mui/icons-material/Save"; import DeleteIcon from "@mui/icons-material/Delete"; -import ConfirmationDialog from "../../../../common/dialogs/ConfirmationDialog"; +import ConfirmationDialog from "../../../../../common/dialogs/ConfirmationDialog"; import { Title, Fields, @@ -21,9 +21,9 @@ import { StyledVisibilityIcon, StyledDeleteIcon, } from "../share/styles"; -import { LinkCreationInfo } from "../../../../../common/types"; +import { LinkCreationInfo } from "../../../../../../common/types"; import { XpansionCandidate } from "../types"; -import SelectSingle from "../../../../common/SelectSingle"; +import SelectSingle from "../../../../../common/SelectSingle"; interface PropType { candidate: XpansionCandidate | undefined; diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx index 072efbc5d2..f2357deadd 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { TextField, Button, Box, Divider, ButtonGroup } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { LinkCreationInfo } from "../../../../../common/types"; +import { LinkCreationInfo } from "../../../../../../common/types"; import { XpansionCandidate } from "../types"; -import SelectSingle from "../../../../common/SelectSingle"; +import SelectSingle from "../../../../../common/SelectSingle"; import { HoverButton, ActiveButton } from "../share/styles"; -import BasicDialog from "../../../../common/dialogs/BasicDialog"; +import BasicDialog from "../../../../../common/dialogs/BasicDialog"; interface PropType { open: boolean; diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx index e33b5fad65..f03ad1c4d0 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx @@ -2,9 +2,9 @@ import { useState } from "react"; import { Box, Button } from "@mui/material"; import { useTranslation } from "react-i18next"; import DeleteIcon from "@mui/icons-material/Delete"; -import PropertiesView from "../../../../common/PropertiesView"; +import PropertiesView from "../../../../../common/PropertiesView"; import { XpansionCandidate } from "../types"; -import ConfirmationDialog from "../../../../common/dialogs/ConfirmationDialog"; +import ConfirmationDialog from "../../../../../common/dialogs/ConfirmationDialog"; import ListElement from "../../common/ListElement"; interface PropsType { diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx similarity index 92% rename from webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx index a40b78b2f7..700f7b0e1d 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx @@ -6,9 +6,9 @@ import { useTranslation } from "react-i18next"; import { Backdrop, Box, CircularProgress } from "@mui/material"; import { usePromise as usePromiseWrapper } from "react-use"; import { useSnackbar } from "notistack"; -import { MatrixType, StudyMetadata } from "../../../../../common/types"; +import { MatrixType, StudyMetadata } from "../../../../../../common/types"; import { XpansionCandidate } from "../types"; -import SplitLayoutView from "../../../../common/SplitLayoutView"; +import SplitLayoutView from "../../../../../common/SplitLayoutView"; import { getAllCandidates, getAllCapacities, @@ -18,18 +18,18 @@ import { updateCandidate, getCapacity, xpansionConfigurationExist, -} from "../../../../../services/api/xpansion"; +} from "../../../../../../services/api/xpansion"; import { transformNameToId, removeEmptyFields, -} from "../../../../../services/utils/index"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import { getAllLinks } from "../../../../../services/api/studydata"; +} from "../../../../../../services/utils/index"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import { getAllLinks } from "../../../../../../services/api/studydata"; import XpansionPropsView from "./XpansionPropsView"; import CreateCandidateDialog from "./CreateCandidateDialog"; import CandidateForm from "./CandidateForm"; -import usePromiseWithSnackbarError from "../../../../../hooks/usePromiseWithSnackbarError"; -import DataViewerDialog from "../../../../common/dialogs/DataViewerDialog"; +import usePromiseWithSnackbarError from "../../../../../../hooks/usePromiseWithSnackbarError"; +import DataViewerDialog from "../../../../../common/dialogs/DataViewerDialog"; function Candidates() { const [t] = useTranslation(); diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Capacities/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Capacities/index.tsx similarity index 88% rename from webapp/src/components/singlestudy/explore/Xpansion/Capacities/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Capacities/index.tsx index 478ef30468..57722517b1 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Capacities/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Capacities/index.tsx @@ -4,17 +4,17 @@ import { useOutletContext } from "react-router-dom"; import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; import { Box, Paper } from "@mui/material"; -import { MatrixType, StudyMetadata } from "../../../../../common/types"; +import { MatrixType, StudyMetadata } from "../../../../../../common/types"; import { getAllCapacities, deleteCapacity, getCapacity, addCapacity, -} from "../../../../../services/api/xpansion"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import DataViewerDialog from "../../../../common/dialogs/DataViewerDialog"; -import FileTable from "../../../../common/FileTable"; +} from "../../../../../../services/api/xpansion"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import DataViewerDialog from "../../../../../common/dialogs/DataViewerDialog"; +import FileTable from "../../../../../common/FileTable"; import { Title } from "../share/styles"; function Capacities() { diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Files/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Files/index.tsx similarity index 88% rename from webapp/src/components/singlestudy/explore/Xpansion/Files/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Files/index.tsx index 6a8e15ed7d..6ade41131d 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Files/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Files/index.tsx @@ -4,17 +4,17 @@ import { useOutletContext } from "react-router-dom"; import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; import { Box, Paper } from "@mui/material"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; import { getAllConstraints, deleteConstraints, getConstraint, addConstraints, -} from "../../../../../services/api/xpansion"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import FileTable from "../../../../common/FileTable"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import DataViewerDialog from "../../../../common/dialogs/DataViewerDialog"; +} from "../../../../../../services/api/xpansion"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import FileTable from "../../../../../common/FileTable"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import DataViewerDialog from "../../../../../common/dialogs/DataViewerDialog"; import { Title } from "../share/styles"; function Files() { diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Settings/SettingsForm.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx similarity index 99% rename from webapp/src/components/singlestudy/explore/Xpansion/Settings/SettingsForm.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx index b6c867a879..c42cc78323 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Settings/SettingsForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx @@ -9,7 +9,7 @@ import { Title, StyledVisibilityIcon, } from "../share/styles"; -import SelectSingle from "../../../../common/SelectSingle"; +import SelectSingle from "../../../../../common/SelectSingle"; interface PropType { settings: XpansionSettings; diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Settings/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx similarity index 90% rename from webapp/src/components/singlestudy/explore/Xpansion/Settings/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx index 65a8cf842e..721d50ca62 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Settings/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx @@ -5,19 +5,19 @@ import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; import { Box, Paper } from "@mui/material"; import { useSnackbar } from "notistack"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; import { XpansionSettings } from "../types"; import { getXpansionSettings, getAllConstraints, getConstraint, updateXpansionSettings, -} from "../../../../../services/api/xpansion"; +} from "../../../../../../services/api/xpansion"; import SettingsForm from "./SettingsForm"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import { removeEmptyFields } from "../../../../../services/utils/index"; -import DataViewerDialog from "../../../../common/dialogs/DataViewerDialog"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import { removeEmptyFields } from "../../../../../../services/utils/index"; +import DataViewerDialog from "../../../../../common/dialogs/DataViewerDialog"; function Settings() { const [t] = useTranslation(); diff --git a/webapp/src/components/singlestudy/explore/Xpansion/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/index.tsx similarity index 93% rename from webapp/src/components/singlestudy/explore/Xpansion/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/index.tsx index 96ec4f48d0..d36b29e1ed 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/index.tsx @@ -4,12 +4,12 @@ import { AxiosError } from "axios"; import { useOutletContext } from "react-router-dom"; import { Box, Button } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { StudyMetadata } from "../../../../common/types"; +import { StudyMetadata } from "../../../../../common/types"; import { createXpansionConfiguration, xpansionConfigurationExist, -} from "../../../../services/api/xpansion"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +} from "../../../../../services/api/xpansion"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; import TabWrapper from "../TabWrapper"; function Xpansion() { diff --git a/webapp/src/components/singlestudy/explore/Xpansion/share/styles.ts b/webapp/src/components/App/Singlestudy/explore/Xpansion/share/styles.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Xpansion/share/styles.ts rename to webapp/src/components/App/Singlestudy/explore/Xpansion/share/styles.ts diff --git a/webapp/src/components/singlestudy/explore/Xpansion/types.ts b/webapp/src/components/App/Singlestudy/explore/Xpansion/types.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Xpansion/types.ts rename to webapp/src/components/App/Singlestudy/explore/Xpansion/types.ts diff --git a/webapp/src/components/singlestudy/explore/common/ListElement.tsx b/webapp/src/components/App/Singlestudy/explore/common/ListElement.tsx similarity index 100% rename from webapp/src/components/singlestudy/explore/common/ListElement.tsx rename to webapp/src/components/App/Singlestudy/explore/common/ListElement.tsx diff --git a/webapp/src/components/singlestudy/explore/hooks/useStudyData.ts b/webapp/src/components/App/Singlestudy/explore/hooks/useStudyData.ts similarity index 76% rename from webapp/src/components/singlestudy/explore/hooks/useStudyData.ts rename to webapp/src/components/App/Singlestudy/explore/hooks/useStudyData.ts index 76acc71792..5dd59dd20e 100644 --- a/webapp/src/components/singlestudy/explore/hooks/useStudyData.ts +++ b/webapp/src/components/App/Singlestudy/explore/hooks/useStudyData.ts @@ -2,11 +2,11 @@ import { useEffect, useState } from "react"; import { FileStudyTreeConfigDTO, StudyMetadata, -} from "../../../../common/types"; -import { createStudyData } from "../../../../redux/ducks/studyDataSynthesis"; -import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; -import useAppSelector from "../../../../redux/hooks/useAppSelector"; -import { getStudyData } from "../../../../redux/selectors"; +} from "../../../../../common/types"; +import { createStudyData } from "../../../../../redux/ducks/studyDataSynthesis"; +import useAppDispatch from "../../../../../redux/hooks/useAppDispatch"; +import useAppSelector from "../../../../../redux/hooks/useAppSelector"; +import { getStudyData } from "../../../../../redux/selectors"; interface Props { studyId: StudyMetadata["id"]; diff --git a/webapp/src/pages/SingleStudy/index.tsx b/webapp/src/components/App/Singlestudy/index.tsx similarity index 85% rename from webapp/src/pages/SingleStudy/index.tsx rename to webapp/src/components/App/Singlestudy/index.tsx index 3a13304781..6621b23c2c 100644 --- a/webapp/src/pages/SingleStudy/index.tsx +++ b/webapp/src/components/App/Singlestudy/index.tsx @@ -11,21 +11,21 @@ import { VariantTree, WSEvent, WSMessage, -} from "../../common/types"; -import { getStudyMetadata } from "../../services/api/study"; -import NavHeader from "../../components/singlestudy/NavHeader"; +} from "../../../common/types"; +import { getStudyMetadata } from "../../../services/api/study"; +import NavHeader from "./NavHeader"; import { getVariantChildren, getVariantParents, -} from "../../services/api/variant"; -import TabWrapper from "../../components/singlestudy/explore/TabWrapper"; -import HomeView from "../../components/singlestudy/HomeView"; -import { setCurrentStudy } from "../../redux/ducks/studies"; -import { findNodeInTree } from "../../services/utils"; -import CommandDrawer from "../../components/singlestudy/Commands"; -import { addWsMessageListener } from "../../services/webSockets"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import SimpleLoader from "../../components/common/loaders/SimpleLoader"; +} from "../../../services/api/variant"; +import TabWrapper from "./explore/TabWrapper"; +import HomeView from "./HomeView"; +import { setCurrentStudy } from "../../../redux/ducks/studies"; +import { findNodeInTree } from "../../../services/utils"; +import CommandDrawer from "./Commands"; +import { addWsMessageListener } from "../../../services/webSockets"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import SimpleLoader from "../../common/loaders/SimpleLoader"; const logError = debug("antares:singlestudy:error"); diff --git a/webapp/src/components/studies/BatchModeMenu.tsx b/webapp/src/components/App/Studies/BatchModeMenu.tsx similarity index 100% rename from webapp/src/components/studies/BatchModeMenu.tsx rename to webapp/src/components/App/Studies/BatchModeMenu.tsx diff --git a/webapp/src/components/studies/CreateStudyDialog/index.tsx b/webapp/src/components/App/Studies/CreateStudyDialog/index.tsx similarity index 86% rename from webapp/src/components/studies/CreateStudyDialog/index.tsx rename to webapp/src/components/App/Studies/CreateStudyDialog/index.tsx index 566e2e78f1..d8c32e7d45 100644 --- a/webapp/src/components/studies/CreateStudyDialog/index.tsx +++ b/webapp/src/components/App/Studies/CreateStudyDialog/index.tsx @@ -6,21 +6,25 @@ import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; import { usePromise } from "react-use"; import * as R from "ramda"; -import SingleSelect from "../../common/SelectSingle"; -import MultiSelect from "../../common/SelectMulti"; -import { GenericInfo, GroupDTO, StudyPublicMode } from "../../../common/types"; -import TextSeparator from "../../common/TextSeparator"; -import { getGroups } from "../../../services/api/user"; -import TagTextInput from "../../common/TagTextInput"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import SingleSelect from "../../../common/SelectSingle"; +import MultiSelect from "../../../common/SelectMulti"; +import { + GenericInfo, + GroupDTO, + StudyPublicMode, +} from "../../../../common/types"; +import TextSeparator from "../../../common/TextSeparator"; +import { getGroups } from "../../../../services/api/user"; +import TagTextInput from "../../../common/TagTextInput"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; import BasicDialog, { BasicDialogProps, -} from "../../common/dialogs/BasicDialog"; +} from "../../../common/dialogs/BasicDialog"; import { Root, ElementContainer, InputElement } from "./style"; -import { createStudy } from "../../../redux/ducks/studies"; -import { getStudyVersionsFormatted } from "../../../redux/selectors"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import { createStudy } from "../../../../redux/ducks/studies"; +import { getStudyVersionsFormatted } from "../../../../redux/selectors"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; const logErr = debug("antares:createstudyform:error"); diff --git a/webapp/src/components/studies/CreateStudyDialog/style.ts b/webapp/src/components/App/Studies/CreateStudyDialog/style.ts similarity index 100% rename from webapp/src/components/studies/CreateStudyDialog/style.ts rename to webapp/src/components/App/Studies/CreateStudyDialog/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx similarity index 94% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx index 183eaddceb..d36463a8a8 100644 --- a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx @@ -1,8 +1,8 @@ import { Box, Chip, ListItem } from "@mui/material"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import SelectSingle from "../../../../../common/SelectSingle"; -import TextSeparator from "../../../../../common/TextSeparator"; +import SelectSingle from "../../../../../../common/SelectSingle"; +import TextSeparator from "../../../../../../common/TextSeparator"; import { AddIcon } from "../../TagSelect/style"; import { FilterLinkContainer, Root, Container } from "./style"; diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/style.ts b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/style.ts similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/style.ts rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx similarity index 94% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx index bc3b164de1..0cef1d0190 100644 --- a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx @@ -1,7 +1,7 @@ import { Box, TextField } from "@mui/material"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import TextSeparator from "../../../../../common/TextSeparator"; +import TextSeparator from "../../../../../../common/TextSeparator"; import { Root } from "./style"; interface FilterLink { diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/style.ts b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/style.ts similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/style.ts rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/index.tsx similarity index 96% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/index.tsx index e5f9ad7e60..549e2ea898 100644 --- a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/index.tsx @@ -5,8 +5,8 @@ import { Area, Set, StudyOutputDownloadType, -} from "../../../../../common/types"; -import SelectMulti from "../../../../common/SelectMulti"; +} from "../../../../../../common/types"; +import SelectMulti from "../../../../../common/SelectMulti"; import { Root } from "./style"; import MultipleLinkElement from "./MultipleLinkElement"; import SingleLinkElement from "./SingleLinkElement"; diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/style.ts b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/style.ts similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/style.ts rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/TagSelect/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/TagSelect/index.tsx similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/TagSelect/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/TagSelect/index.tsx diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/TagSelect/style.ts b/webapp/src/components/App/Studies/ExportModal/ExportFilter/TagSelect/style.ts similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/TagSelect/style.ts rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/TagSelect/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/index.tsx similarity index 97% rename from webapp/src/components/studies/ExportModal/ExportFilter/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/index.tsx index 1303990390..d79d350598 100644 --- a/webapp/src/components/studies/ExportModal/ExportFilter/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/ExportFilter/index.tsx @@ -10,11 +10,11 @@ import { StudyOutputDownloadDTO, StudyOutputDownloadLevelDTO, StudyOutputDownloadType, -} from "../../../../common/types"; +} from "../../../../../common/types"; import Filter from "./Filter"; import TagSelect from "./TagSelect"; -import SelectSingle from "../../../common/SelectSingle"; -import SelectMulti from "../../../common/SelectMulti"; +import SelectSingle from "../../../../common/SelectSingle"; +import SelectMulti from "../../../../common/SelectMulti"; const Root = styled(Box)(({ theme }) => ({ flex: 1, diff --git a/webapp/src/components/studies/ExportModal/index.tsx b/webapp/src/components/App/Studies/ExportModal/index.tsx similarity index 95% rename from webapp/src/components/studies/ExportModal/index.tsx rename to webapp/src/components/App/Studies/ExportModal/index.tsx index f81474eb0d..f8ad79a676 100644 --- a/webapp/src/components/studies/ExportModal/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/index.tsx @@ -14,19 +14,19 @@ import { StudyOutputDownloadDTO, StudyOutputDownloadLevelDTO, StudyOutputDownloadType, -} from "../../../common/types"; +} from "../../../../common/types"; import BasicDialog, { BasicDialogProps, -} from "../../common/dialogs/BasicDialog"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import SelectSingle from "../../common/SelectSingle"; +} from "../../../common/dialogs/BasicDialog"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import SelectSingle from "../../../common/SelectSingle"; import { exportStudy, exportOuput as callExportOutput, getStudyOutputs, getStudySynthesis, downloadOutput, -} from "../../../services/api/study"; +} from "../../../../services/api/study"; import ExportFilter from "./ExportFilter"; const logError = debug("antares:studies:card:error"); diff --git a/webapp/src/components/studies/FilterDrawer.tsx b/webapp/src/components/App/Studies/FilterDrawer.tsx similarity index 92% rename from webapp/src/components/studies/FilterDrawer.tsx rename to webapp/src/components/App/Studies/FilterDrawer.tsx index d8e1a9cb53..be3cd70953 100644 --- a/webapp/src/components/studies/FilterDrawer.tsx +++ b/webapp/src/components/App/Studies/FilterDrawer.tsx @@ -12,18 +12,18 @@ import { Typography, } from "@mui/material"; import { useEffect, useRef } from "react"; -import { STUDIES_FILTER_WIDTH } from "../../theme"; -import useAppSelector from "../../redux/hooks/useAppSelector"; +import { STUDIES_FILTER_WIDTH } from "../../../theme"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; import { getGroups, getStudyFilters, getStudyVersions, getUsers, -} from "../../redux/selectors"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import { StudyFilters, updateStudyFilters } from "../../redux/ducks/studies"; -import CheckboxesTagsFE from "../common/fieldEditors/CheckboxesTagsFE"; -import { displayVersionName } from "../../services/utils"; +} from "../../../redux/selectors"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import { StudyFilters, updateStudyFilters } from "../../../redux/ducks/studies"; +import CheckboxesTagsFE from "../../common/fieldEditors/CheckboxesTagsFE"; +import { displayVersionName } from "../../../services/utils"; interface Props { open: boolean; diff --git a/webapp/src/components/studies/HeaderBottom.tsx b/webapp/src/components/App/Studies/HeaderBottom.tsx similarity index 91% rename from webapp/src/components/studies/HeaderBottom.tsx rename to webapp/src/components/App/Studies/HeaderBottom.tsx index b806796444..d62184c9ef 100644 --- a/webapp/src/components/studies/HeaderBottom.tsx +++ b/webapp/src/components/App/Studies/HeaderBottom.tsx @@ -9,13 +9,13 @@ import { import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined"; import { useTranslation } from "react-i18next"; import { indigo, purple } from "@mui/material/colors"; -import useDebounce from "../../hooks/useDebounce"; -import useAppSelector from "../../redux/hooks/useAppSelector"; -import { getGroups, getStudyFilters, getUsers } from "../../redux/selectors"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import { StudyFilters, updateStudyFilters } from "../../redux/ducks/studies"; -import { GroupDTO, UserDTO } from "../../common/types"; -import { displayVersionName } from "../../services/utils"; +import useDebounce from "../../../hooks/useDebounce"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getGroups, getStudyFilters, getUsers } from "../../../redux/selectors"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import { StudyFilters, updateStudyFilters } from "../../../redux/ducks/studies"; +import { GroupDTO, UserDTO } from "../../../common/types"; +import { displayVersionName } from "../../../services/utils"; type PropTypes = { onOpenFilterClick: VoidFunction; diff --git a/webapp/src/components/studies/HeaderTopRight.tsx b/webapp/src/components/App/Studies/HeaderTopRight.tsx similarity index 90% rename from webapp/src/components/studies/HeaderTopRight.tsx rename to webapp/src/components/App/Studies/HeaderTopRight.tsx index 7906961264..fdeec318b1 100644 --- a/webapp/src/components/studies/HeaderTopRight.tsx +++ b/webapp/src/components/App/Studies/HeaderTopRight.tsx @@ -5,11 +5,11 @@ import { useTranslation } from "react-i18next"; import AddCircleOutlineOutlinedIcon from "@mui/icons-material/AddCircleOutlineOutlined"; import GetAppOutlinedIcon from "@mui/icons-material/GetAppOutlined"; import { useSnackbar } from "notistack"; -import { createStudy } from "../../redux/ducks/studies"; -import ImportDialog from "../common/dialogs/ImportDialog"; +import { createStudy } from "../../../redux/ducks/studies"; +import ImportDialog from "../../common/dialogs/ImportDialog"; import CreateStudyDialog from "./CreateStudyDialog"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; function HeaderRight() { const [openCreateDialog, setOpenCreateDialog] = useState(false); diff --git a/webapp/src/components/studies/LauncherDialog.tsx b/webapp/src/components/App/Studies/LauncherDialog.tsx similarity index 96% rename from webapp/src/components/studies/LauncherDialog.tsx rename to webapp/src/components/App/Studies/LauncherDialog.tsx index d5dd25a001..e4578749ea 100644 --- a/webapp/src/components/studies/LauncherDialog.tsx +++ b/webapp/src/components/App/Studies/LauncherDialog.tsx @@ -21,12 +21,12 @@ import { useSnackbar } from "notistack"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useMountedState } from "react-use"; import { shallowEqual } from "react-redux"; -import { StudyMetadata } from "../../common/types"; -import { LaunchOptions, launchStudy } from "../../services/api/study"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import BasicDialog from "../common/dialogs/BasicDialog"; -import useAppSelector from "../../redux/hooks/useAppSelector"; -import { getStudy } from "../../redux/selectors"; +import { StudyMetadata } from "../../../common/types"; +import { LaunchOptions, launchStudy } from "../../../services/api/study"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import BasicDialog from "../../common/dialogs/BasicDialog"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getStudy } from "../../../redux/selectors"; const LAUNCH_DURATION_MAX_HOURS = 240; diff --git a/webapp/src/components/studies/MoveStudyDialog.tsx b/webapp/src/components/App/Studies/MoveStudyDialog.tsx similarity index 86% rename from webapp/src/components/studies/MoveStudyDialog.tsx rename to webapp/src/components/App/Studies/MoveStudyDialog.tsx index 77da38d388..2a7813daac 100644 --- a/webapp/src/components/studies/MoveStudyDialog.tsx +++ b/webapp/src/components/App/Studies/MoveStudyDialog.tsx @@ -4,12 +4,12 @@ import { useSnackbar } from "notistack"; import { dropLast, join, split } from "ramda"; import { useTranslation } from "react-i18next"; import { usePromise } from "react-use"; -import { StudyMetadata } from "../../common/types"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import { moveStudy } from "../../services/api/study"; -import { isStringEmpty } from "../../services/utils"; -import FormDialog from "../common/dialogs/FormDialog"; -import { SubmitHandlerData } from "../common/Form"; +import { StudyMetadata } from "../../../common/types"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import { moveStudy } from "../../../services/api/study"; +import { isStringEmpty } from "../../../services/utils"; +import FormDialog from "../../common/dialogs/FormDialog"; +import { SubmitHandlerData } from "../../common/Form"; interface Props extends DialogProps { study: StudyMetadata; diff --git a/webapp/src/components/studies/SideNav.tsx b/webapp/src/components/App/Studies/SideNav.tsx similarity index 88% rename from webapp/src/components/studies/SideNav.tsx rename to webapp/src/components/App/Studies/SideNav.tsx index 1197c41cf0..d1e2f7ac38 100644 --- a/webapp/src/components/studies/SideNav.tsx +++ b/webapp/src/components/App/Studies/SideNav.tsx @@ -1,10 +1,10 @@ import { useNavigate } from "react-router"; import { Box, Typography, List, ListItem, ListItemText } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { STUDIES_SIDE_NAV_WIDTH } from "../../theme"; +import { STUDIES_SIDE_NAV_WIDTH } from "../../../theme"; import StudyTree from "./StudyTree"; -import useAppSelector from "../../redux/hooks/useAppSelector"; -import { getFavoriteStudies } from "../../redux/selectors"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getFavoriteStudies } from "../../../redux/selectors"; function SideNav() { const favorites = useAppSelector(getFavoriteStudies); diff --git a/webapp/src/components/studies/StudiesList/StudyCardCell.tsx b/webapp/src/components/App/Studies/StudiesList/StudyCardCell.tsx similarity index 97% rename from webapp/src/components/studies/StudiesList/StudyCardCell.tsx rename to webapp/src/components/App/Studies/StudiesList/StudyCardCell.tsx index 48b2ebbb2b..34c3c38ed4 100644 --- a/webapp/src/components/studies/StudiesList/StudyCardCell.tsx +++ b/webapp/src/components/App/Studies/StudiesList/StudyCardCell.tsx @@ -1,7 +1,7 @@ import { memo } from "react"; import { Box, Skeleton } from "@mui/material"; import { GridChildComponentProps, areEqual } from "react-window"; -import { StudyMetadata } from "../../../common/types"; +import { StudyMetadata } from "../../../../common/types"; import StudyCard from "../StudyCard"; import { StudiesListProps } from "."; diff --git a/webapp/src/components/studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx similarity index 96% rename from webapp/src/components/studies/StudiesList/index.tsx rename to webapp/src/components/App/Studies/StudiesList/index.tsx index 17e3190eb6..34c9f7e98a 100644 --- a/webapp/src/components/studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -24,27 +24,27 @@ import FolderOffIcon from "@mui/icons-material/FolderOff"; import RefreshIcon from "@mui/icons-material/Refresh"; import { FixedSizeGrid, GridOnScrollProps } from "react-window"; import { v4 as uuidv4 } from "uuid"; -import { StudyMetadata } from "../../../common/types"; +import { StudyMetadata } from "../../../../common/types"; import { STUDIES_HEIGHT_HEADER, STUDIES_LIST_HEADER_HEIGHT, -} from "../../../theme"; +} from "../../../../theme"; import { fetchStudies, setStudyScrollPosition, StudiesSortConf, updateStudiesSortConf, updateStudyFilters, -} from "../../../redux/ducks/studies"; +} from "../../../../redux/ducks/studies"; import LauncherDialog from "../LauncherDialog"; -import useDebounce from "../../../hooks/useDebounce"; +import useDebounce from "../../../../hooks/useDebounce"; import { getStudiesScrollPosition, getStudiesSortConf, getStudyFilters, -} from "../../../redux/selectors"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +} from "../../../../redux/selectors"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; import StudyCardCell from "./StudyCardCell"; import BatchModeMenu from "../BatchModeMenu"; diff --git a/webapp/src/components/studies/StudyCard.tsx b/webapp/src/components/App/Studies/StudyCard.tsx similarity index 96% rename from webapp/src/components/studies/StudyCard.tsx rename to webapp/src/components/App/Studies/StudyCard.tsx index 89778d0c42..49ce42c2fb 100644 --- a/webapp/src/components/studies/StudyCard.tsx +++ b/webapp/src/components/App/Studies/StudyCard.tsx @@ -34,22 +34,22 @@ import BoltIcon from "@mui/icons-material/Bolt"; import FileCopyOutlinedIcon from "@mui/icons-material/FileCopyOutlined"; import debug from "debug"; import { areEqual } from "react-window"; -import { StudyMetadata } from "../../common/types"; +import { StudyMetadata } from "../../../common/types"; import { buildModificationDate, convertUTCToLocalTime, displayVersionName, -} from "../../services/utils"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; +} from "../../../services/utils"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; import ExportModal from "./ExportModal"; -import StarToggle from "../common/StarToggle"; +import StarToggle from "../../common/StarToggle"; import MoveStudyDialog from "./MoveStudyDialog"; -import ConfirmationDialog from "../common/dialogs/ConfirmationDialog"; -import useAppSelector from "../../redux/hooks/useAppSelector"; -import { getStudy, isStudyFavorite } from "../../redux/selectors"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import { deleteStudy, toggleFavorite } from "../../redux/ducks/studies"; -import * as studyApi from "../../services/api/study"; +import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getStudy, isStudyFavorite } from "../../../redux/selectors"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import { deleteStudy, toggleFavorite } from "../../../redux/ducks/studies"; +import * as studyApi from "../../../services/api/study"; const logError = debug("antares:studieslist:error"); diff --git a/webapp/src/components/studies/StudyTree.tsx b/webapp/src/components/App/Studies/StudyTree.tsx similarity index 92% rename from webapp/src/components/studies/StudyTree.tsx rename to webapp/src/components/App/Studies/StudyTree.tsx index fb3e1cdc21..f2de623eff 100644 --- a/webapp/src/components/studies/StudyTree.tsx +++ b/webapp/src/components/App/Studies/StudyTree.tsx @@ -12,12 +12,12 @@ import TreeItem from "@mui/lab/TreeItem"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; import { StudyTreeNode } from "./utils"; -import { scanFolder } from "../../services/api/study"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import useAppSelector from "../../redux/hooks/useAppSelector"; -import { getStudiesTree, getStudyFilters } from "../../redux/selectors"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import { updateStudyFilters } from "../../redux/ducks/studies"; +import { scanFolder } from "../../../services/api/study"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getStudiesTree, getStudyFilters } from "../../../redux/selectors"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import { updateStudyFilters } from "../../../redux/ducks/studies"; function StudyTree() { const folder = useAppSelector((state) => getStudyFilters(state).folder); diff --git a/webapp/src/pages/Studies/index.tsx b/webapp/src/components/App/Studies/index.tsx similarity index 75% rename from webapp/src/pages/Studies/index.tsx rename to webapp/src/components/App/Studies/index.tsx index 3bd0759dbd..c6f00afaba 100644 --- a/webapp/src/pages/Studies/index.tsx +++ b/webapp/src/components/App/Studies/index.tsx @@ -3,20 +3,20 @@ import * as R from "ramda"; import { Box, Divider } from "@mui/material"; import { useTranslation } from "react-i18next"; import TravelExploreOutlinedIcon from "@mui/icons-material/TravelExploreOutlined"; -import SideNav from "../../components/studies/SideNav"; -import StudiesList from "../../components/studies/StudiesList"; -import { fetchStudies } from "../../redux/ducks/studies"; -import RootPage from "../../components/common/page/RootPage"; -import HeaderTopRight from "../../components/studies/HeaderTopRight"; -import HeaderBottom from "../../components/studies/HeaderBottom"; -import SimpleLoader from "../../components/common/loaders/SimpleLoader"; +import SideNav from "./SideNav"; +import StudiesList from "./StudiesList"; +import { fetchStudies } from "../../../redux/ducks/studies"; +import RootPage from "../../common/page/RootPage"; +import HeaderTopRight from "./HeaderTopRight"; +import HeaderBottom from "./HeaderBottom"; +import SimpleLoader from "../../common/loaders/SimpleLoader"; import { getStudiesState, getStudyIdsFilteredAndSorted, -} from "../../redux/selectors"; -import { FetchStatus } from "../../redux/utils"; -import useAsyncAppSelector from "../../redux/hooks/useAsyncAppSelector"; -import FilterDrawer from "../../components/studies/FilterDrawer"; +} from "../../../redux/selectors"; +import { FetchStatus } from "../../../redux/utils"; +import useAsyncAppSelector from "../../../redux/hooks/useAsyncAppSelector"; +import FilterDrawer from "./FilterDrawer"; function Studies() { const [t] = useTranslation(); diff --git a/webapp/src/components/studies/utils.ts b/webapp/src/components/App/Studies/utils.ts similarity index 94% rename from webapp/src/components/studies/utils.ts rename to webapp/src/components/App/Studies/utils.ts index 93b300cf59..36a5a15cdc 100644 --- a/webapp/src/components/studies/utils.ts +++ b/webapp/src/components/App/Studies/utils.ts @@ -1,5 +1,5 @@ /* eslint-disable no-plusplus */ -import { StudyMetadata } from "../../common/types"; +import { StudyMetadata } from "../../../common/types"; export interface StudyTreeNode { name: string; @@ -52,5 +52,3 @@ export const buildStudyTree = ( } return tree; }; - -export default {}; diff --git a/webapp/src/components/tasks/JobTableView.tsx b/webapp/src/components/App/Tasks/JobTableView.tsx similarity index 99% rename from webapp/src/components/tasks/JobTableView.tsx rename to webapp/src/components/App/Tasks/JobTableView.tsx index e4b1cf59c3..4f3bc3f676 100644 --- a/webapp/src/components/tasks/JobTableView.tsx +++ b/webapp/src/components/App/Tasks/JobTableView.tsx @@ -24,7 +24,7 @@ import RefreshIcon from "@mui/icons-material/Refresh"; import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import { grey } from "@mui/material/colors"; -import { TaskView, TaskType } from "../../common/types"; +import { TaskView, TaskType } from "../../../common/types"; interface PropType { content: Array; diff --git a/webapp/src/components/tasks/LaunchJobLogView.tsx b/webapp/src/components/App/Tasks/LaunchJobLogView.tsx similarity index 93% rename from webapp/src/components/tasks/LaunchJobLogView.tsx rename to webapp/src/components/App/Tasks/LaunchJobLogView.tsx index a7c37ced12..56945ac2f0 100644 --- a/webapp/src/components/tasks/LaunchJobLogView.tsx +++ b/webapp/src/components/App/Tasks/LaunchJobLogView.tsx @@ -4,10 +4,10 @@ import { useTranslation } from "react-i18next"; import { Box, Tooltip } from "@mui/material"; import ErrorIcon from "@mui/icons-material/Error"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; -import { getStudyJobLog } from "../../services/api/study"; -import LogModal from "../common/LogModal"; -import { LaunchJob } from "../../common/types"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; +import { getStudyJobLog } from "../../../services/api/study"; +import LogModal from "../../common/LogModal"; +import { LaunchJob } from "../../../common/types"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; interface PropsType { job: LaunchJob; diff --git a/webapp/src/components/tasks/NotificationBadge.tsx b/webapp/src/components/App/Tasks/NotificationBadge.tsx similarity index 88% rename from webapp/src/components/tasks/NotificationBadge.tsx rename to webapp/src/components/App/Tasks/NotificationBadge.tsx index 46dcf0cae6..27a94b97de 100644 --- a/webapp/src/components/tasks/NotificationBadge.tsx +++ b/webapp/src/components/App/Tasks/NotificationBadge.tsx @@ -6,16 +6,16 @@ import { useLocation } from "react-router-dom"; import CircleIcon from "@mui/icons-material/Circle"; import { useSnackbar, VariantType } from "notistack"; import { red } from "@mui/material/colors"; -import { TaskEventPayload, WSEvent, WSMessage } from "../../common/types"; -import { getTask } from "../../services/api/tasks"; -import { addWsMessageListener } from "../../services/webSockets"; +import { TaskEventPayload, WSEvent, WSMessage } from "../../../common/types"; +import { getTask } from "../../../services/api/tasks"; +import { addWsMessageListener } from "../../../services/webSockets"; import { incrementTaskNotifications, resetTaskNotifications, -} from "../../redux/ducks/ui"; -import { getTaskNotificationsCount } from "../../redux/selectors"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import useAppSelector from "../../redux/hooks/useAppSelector"; +} from "../../../redux/ducks/ui"; +import { getTaskNotificationsCount } from "../../../redux/selectors"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; const logError = debug("antares:downloadbadge:error"); diff --git a/webapp/src/pages/Tasks.tsx b/webapp/src/components/App/Tasks/index.tsx similarity index 93% rename from webapp/src/pages/Tasks.tsx rename to webapp/src/components/App/Tasks/index.tsx index 48fc9c2aba..df5bd50188 100644 --- a/webapp/src/pages/Tasks.tsx +++ b/webapp/src/components/App/Tasks/index.tsx @@ -21,30 +21,30 @@ import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; import EventAvailableIcon from "@mui/icons-material/EventAvailable"; import DownloadIcon from "@mui/icons-material/Download"; import { grey } from "@mui/material/colors"; -import RootPage from "../components/common/page/RootPage"; -import SimpleLoader from "../components/common/loaders/SimpleLoader"; -import DownloadLink from "../components/common/DownloadLink"; -import LogModal from "../components/common/LogModal"; +import RootPage from "../../common/page/RootPage"; +import SimpleLoader from "../../common/loaders/SimpleLoader"; +import DownloadLink from "../../common/DownloadLink"; +import LogModal from "../../common/LogModal"; import { addWsMessageListener, sendWsSubscribeMessage, WsChannel, -} from "../services/webSockets"; -import JobTableView from "../components/tasks/JobTableView"; -import { convertUTCToLocalTime } from "../services/utils/index"; +} from "../../../services/webSockets"; +import JobTableView from "./JobTableView"; +import { convertUTCToLocalTime } from "../../../services/utils/index"; import { downloadJobOutput, killStudy, getStudyJobs, -} from "../services/api/study"; +} from "../../../services/api/study"; import { convertFileDownloadDTO, FileDownload, getDownloadUrl, FileDownloadDTO, getDownloadsList, -} from "../services/api/downloads"; -import { fetchStudies } from "../redux/ducks/studies"; +} from "../../../services/api/downloads"; +import { fetchStudies } from "../../../redux/ducks/studies"; import { LaunchJob, TaskDTO, @@ -53,14 +53,14 @@ import { WSMessage, TaskType, TaskStatus, -} from "../common/types"; -import { getAllMiscRunningTasks, getTask } from "../services/api/tasks"; -import LaunchJobLogView from "../components/tasks/LaunchJobLogView"; -import useEnqueueErrorSnackbar from "../hooks/useEnqueueErrorSnackbar"; -import { getStudies } from "../redux/selectors"; -import ConfirmationDialog from "../components/common/dialogs/ConfirmationDialog"; -import useAppSelector from "../redux/hooks/useAppSelector"; -import useAppDispatch from "../redux/hooks/useAppDispatch"; +} from "../../../common/types"; +import { getAllMiscRunningTasks, getTask } from "../../../services/api/tasks"; +import LaunchJobLogView from "./LaunchJobLogView"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import { getStudies } from "../../../redux/selectors"; +import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; const logError = debug("antares:studymanagement:error"); diff --git a/webapp/src/App.tsx b/webapp/src/components/App/index.tsx similarity index 65% rename from webapp/src/App.tsx rename to webapp/src/components/App/index.tsx index 884c467c05..af604a2683 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/components/App/index.tsx @@ -7,39 +7,39 @@ import { } from "react-router-dom"; import { CssBaseline, ThemeProvider } from "@mui/material"; import { SnackbarProvider } from "notistack"; -import maintheme from "./theme"; -import MenuWrapper from "./pages/wrappers/MenuWrapper"; -import Studies from "./pages/Studies"; -import Data from "./pages/Data"; -import Tasks from "./pages/Tasks"; -import Settings from "./pages/Settings"; -import Api from "./pages/Api"; -import LoginWrapper from "./pages/wrappers/LoginWrapper"; -import MaintenanceWrapper from "./pages/wrappers/MaintenanceWrapper"; -import SingleStudy from "./pages/SingleStudy"; -import Modelization from "./components/singlestudy/explore/Modelization"; -import Results from "./components/singlestudy/explore/Results"; -import Configuration from "./components/singlestudy/explore/Configuration"; -import BindingConstraints from "./components/singlestudy/explore/Modelization/BindingConstraints"; -import Links from "./components/singlestudy/explore/Modelization/Links"; -import Areas from "./components/singlestudy/explore/Modelization/Areas"; -import Map from "./components/singlestudy/explore/Modelization/Map"; -import DebugView from "./components/singlestudy/explore/Modelization/DebugView"; -import Xpansion from "./components/singlestudy/explore/Xpansion"; -import Candidates from "./components/singlestudy/explore/Xpansion/Candidates"; -import XpansionSettings from "./components/singlestudy/explore/Xpansion/Settings"; -import Capacities from "./components/singlestudy/explore/Xpansion/Capacities"; -import Files from "./components/singlestudy/explore/Xpansion/Files"; -import Properties from "./components/singlestudy/explore/Modelization/Areas/Properties"; -import Load from "./components/singlestudy/explore/Modelization/Areas/Load"; -import Thermal from "./components/singlestudy/explore/Modelization/Areas/Thermal"; -import Hydro from "./components/singlestudy/explore/Modelization/Areas/Hydro"; -import MiscGen from "./components/singlestudy/explore/Modelization/Areas/MiscGen"; -import Reserve from "./components/singlestudy/explore/Modelization/Areas/Reserve"; -import Wind from "./components/singlestudy/explore/Modelization/Areas/Wind"; -import Solar from "./components/singlestudy/explore/Modelization/Areas/Solar"; -import Renewables from "./components/singlestudy/explore/Modelization/Areas/Renewables"; -import ResultDetails from "./components/singlestudy/explore/Results/ResultDetails"; +import maintheme from "../../theme"; +import MenuWrapper from "../wrappers/MenuWrapper"; +import Studies from "./Studies"; +import Data from "./Data"; +import Tasks from "./Tasks"; +import Settings from "./Settings"; +import Api from "./Api"; +import LoginWrapper from "../wrappers/LoginWrapper"; +import MaintenanceWrapper from "../wrappers/MaintenanceWrapper"; +import SingleStudy from "./Singlestudy"; +import Modelization from "./Singlestudy/explore/Modelization"; +import Results from "./Singlestudy/explore/Results"; +import Configuration from "./Singlestudy/explore/Configuration"; +import BindingConstraints from "./Singlestudy/explore/Modelization/BindingConstraints"; +import Links from "./Singlestudy/explore/Modelization/Links"; +import Areas from "./Singlestudy/explore/Modelization/Areas"; +import Map from "./Singlestudy/explore/Modelization/Map"; +import DebugView from "./Singlestudy/explore/Modelization/DebugView"; +import Xpansion from "./Singlestudy/explore/Xpansion"; +import Candidates from "./Singlestudy/explore/Xpansion/Candidates"; +import XpansionSettings from "./Singlestudy/explore/Xpansion/Settings"; +import Capacities from "./Singlestudy/explore/Xpansion/Capacities"; +import Files from "./Singlestudy/explore/Xpansion/Files"; +import Properties from "./Singlestudy/explore/Modelization/Areas/Properties"; +import Load from "./Singlestudy/explore/Modelization/Areas/Load"; +import Thermal from "./Singlestudy/explore/Modelization/Areas/Thermal"; +import Hydro from "./Singlestudy/explore/Modelization/Areas/Hydro"; +import MiscGen from "./Singlestudy/explore/Modelization/Areas/MiscGen"; +import Reserve from "./Singlestudy/explore/Modelization/Areas/Reserve"; +import Wind from "./Singlestudy/explore/Modelization/Areas/Wind"; +import Solar from "./Singlestudy/explore/Modelization/Areas/Solar"; +import Renewables from "./Singlestudy/explore/Modelization/Areas/Renewables"; +import ResultDetails from "./Singlestudy/explore/Results/ResultDetails"; function App() { return ( diff --git a/webapp/src/components/singlestudy/explore/Modelization/BindingConstraints/index.tsx b/webapp/src/components/singlestudy/explore/Modelization/BindingConstraints/index.tsx deleted file mode 100644 index 48211c72d4..0000000000 --- a/webapp/src/components/singlestudy/explore/Modelization/BindingConstraints/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import UnderConstruction from "../../../../common/page/UnderConstruction"; -import previewImage from "./preview.png"; - -function BindingConstraint() { - return ; -} - -export default BindingConstraint; diff --git a/webapp/src/pages/wrappers/LoginWrapper.tsx b/webapp/src/components/wrappers/LoginWrapper.tsx similarity index 97% rename from webapp/src/pages/wrappers/LoginWrapper.tsx rename to webapp/src/components/wrappers/LoginWrapper.tsx index 9d5014d291..982203b4a1 100644 --- a/webapp/src/pages/wrappers/LoginWrapper.tsx +++ b/webapp/src/components/wrappers/LoginWrapper.tsx @@ -11,8 +11,8 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { login } from "../../redux/ducks/auth"; import logo from "../../assets/logo.png"; import topRightBackground from "../../assets/top-right-background.png"; -import GlobalPageLoadingError from "../../components/common/loaders/GlobalPageLoadingError"; -import AppLoader from "../../components/common/loaders/AppLoader"; +import GlobalPageLoadingError from "../common/loaders/GlobalPageLoadingError"; +import AppLoader from "../common/loaders/AppLoader"; import { needAuth } from "../../services/api/auth"; import { getAuthUser } from "../../redux/selectors"; import usePromiseWithSnackbarError from "../../hooks/usePromiseWithSnackbarError"; diff --git a/webapp/src/pages/wrappers/MaintenanceWrapper/MessageInfoDialog.tsx b/webapp/src/components/wrappers/MaintenanceWrapper/MessageInfoDialog.tsx similarity index 97% rename from webapp/src/pages/wrappers/MaintenanceWrapper/MessageInfoDialog.tsx rename to webapp/src/components/wrappers/MaintenanceWrapper/MessageInfoDialog.tsx index 437ed433a3..be4a447739 100644 --- a/webapp/src/pages/wrappers/MaintenanceWrapper/MessageInfoDialog.tsx +++ b/webapp/src/components/wrappers/MaintenanceWrapper/MessageInfoDialog.tsx @@ -5,7 +5,7 @@ import { AxiosError } from "axios"; import { isStringEmpty, isUserAdmin } from "../../../services/utils"; import { getMessageInfo as getMessageInfoAPI } from "../../../services/api/maintenance"; import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import OkDialog from "../../../components/common/dialogs/OkDialog"; +import OkDialog from "../../common/dialogs/OkDialog"; import { setMessageInfo } from "../../../redux/ducks/ui"; import { getAuthUser, getMessageInfo } from "../../../redux/selectors"; import useAppSelector from "../../../redux/hooks/useAppSelector"; diff --git a/webapp/src/pages/wrappers/MaintenanceWrapper/Stars.css b/webapp/src/components/wrappers/MaintenanceWrapper/Stars.css similarity index 100% rename from webapp/src/pages/wrappers/MaintenanceWrapper/Stars.css rename to webapp/src/components/wrappers/MaintenanceWrapper/Stars.css diff --git a/webapp/src/pages/wrappers/MaintenanceWrapper/Stars.tsx b/webapp/src/components/wrappers/MaintenanceWrapper/Stars.tsx similarity index 100% rename from webapp/src/pages/wrappers/MaintenanceWrapper/Stars.tsx rename to webapp/src/components/wrappers/MaintenanceWrapper/Stars.tsx diff --git a/webapp/src/pages/wrappers/MaintenanceWrapper/index.tsx b/webapp/src/components/wrappers/MaintenanceWrapper/index.tsx similarity index 100% rename from webapp/src/pages/wrappers/MaintenanceWrapper/index.tsx rename to webapp/src/components/wrappers/MaintenanceWrapper/index.tsx diff --git a/webapp/src/pages/wrappers/MenuWrapper/index.tsx b/webapp/src/components/wrappers/MenuWrapper/index.tsx similarity index 98% rename from webapp/src/pages/wrappers/MenuWrapper/index.tsx rename to webapp/src/components/wrappers/MenuWrapper/index.tsx index d69f9380f5..b722cd93e7 100644 --- a/webapp/src/pages/wrappers/MenuWrapper/index.tsx +++ b/webapp/src/components/wrappers/MenuWrapper/index.tsx @@ -27,7 +27,7 @@ import { } from "@mui/material"; import { useMount } from "react-use"; import logo from "../../../assets/logo.png"; -import NotificationBadge from "../../../components/tasks/NotificationBadge"; +import NotificationBadge from "../../App/Tasks/NotificationBadge"; import topRightBackground from "../../../assets/top-right-background.png"; import { setMenuExtensionStatus } from "../../../redux/ducks/ui"; import { @@ -48,7 +48,7 @@ import { getMenuExtended, getWebSocketConnected, } from "../../../redux/selectors"; -import ConfirmationDialog from "../../../components/common/dialogs/ConfirmationDialog"; +import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; import { logout } from "../../../redux/ducks/auth"; import useAppSelector from "../../../redux/hooks/useAppSelector"; import useAppDispatch from "../../../redux/hooks/useAppDispatch"; diff --git a/webapp/src/pages/wrappers/MenuWrapper/styles.ts b/webapp/src/components/wrappers/MenuWrapper/styles.ts similarity index 100% rename from webapp/src/pages/wrappers/MenuWrapper/styles.ts rename to webapp/src/components/wrappers/MenuWrapper/styles.ts diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index b40b08bd60..05292f7ee8 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -3,7 +3,7 @@ import { Provider } from "react-redux"; import { StyledEngineProvider } from "@mui/material"; import { initI18n } from "./i18n"; import "./index.css"; -import App from "./App"; +import App from "./components/App"; import { Config, initConfig } from "./services/config"; import storage, { StorageKey } from "./services/utils/localStorage"; import store from "./redux/store"; diff --git a/webapp/src/redux/selectors.ts b/webapp/src/redux/selectors.ts index 9b89d598b7..afb4b6cdf1 100644 --- a/webapp/src/redux/selectors.ts +++ b/webapp/src/redux/selectors.ts @@ -6,8 +6,8 @@ import { StudyMetadata, UserDetailsDTO, } from "../common/types"; -import { buildStudyTree } from "../components/studies/utils"; -import { filterStudies, sortStudies } from "../pages/Studies/utils"; +import { buildStudyTree } from "../components/App/Studies/utils"; +import { filterStudies, sortStudies } from "../utils/studiesUtils"; import { convertVersions, isGroupAdmin, isUserAdmin } from "../services/utils"; import { AppState } from "./ducks"; import { AuthState } from "./ducks/auth"; diff --git a/webapp/src/services/api/xpansion.ts b/webapp/src/services/api/xpansion.ts index f2dffcfb32..744bdbf365 100644 --- a/webapp/src/services/api/xpansion.ts +++ b/webapp/src/services/api/xpansion.ts @@ -3,7 +3,7 @@ import { MatrixType } from "../../common/types"; import { XpansionCandidate, XpansionSettings, -} from "../../components/singlestudy/explore/Xpansion/types"; +} from "../../components/App/Singlestudy/explore/Xpansion/types"; import client from "./client"; export const createXpansionConfiguration = async ( diff --git a/webapp/src/pages/Studies/utils.ts b/webapp/src/utils/studiesUtils.ts similarity index 96% rename from webapp/src/pages/Studies/utils.ts rename to webapp/src/utils/studiesUtils.ts index 7b7b50334d..4096d0c71c 100644 --- a/webapp/src/pages/Studies/utils.ts +++ b/webapp/src/utils/studiesUtils.ts @@ -1,8 +1,8 @@ import moment from "moment"; import * as R from "ramda"; import * as RA from "ramda-adjunct"; -import { StudyMetadata } from "../../common/types"; -import { StudiesSortConf, StudyFilters } from "../../redux/ducks/studies"; +import { StudyMetadata } from "../common/types"; +import { StudiesSortConf, StudyFilters } from "../redux/ducks/studies"; //////////////////////////////////////////////////////////////// // Sort From e2b66264b3b57ded63e82be1257ea21db3fbbbba Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Fri, 17 Jun 2022 14:49:43 +0200 Subject: [PATCH 04/24] Fix BooleanFE label for general config Signed-off-by: Paul Bui-Quang --- .../explore/Configuration/General/Fields/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx index da7ae7ad46..4047c38d07 100644 --- a/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx @@ -249,16 +249,16 @@ function Fields(props: Props) { <> Date: Fri, 17 Jun 2022 15:29:48 +0200 Subject: [PATCH 05/24] Fix trads Signed-off-by: Paul Bui-Quang --- webapp/src/components/App/Studies/StudyCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyCard.tsx b/webapp/src/components/App/Studies/StudyCard.tsx index 49ce42c2fb..5704341f74 100644 --- a/webapp/src/components/App/Studies/StudyCard.tsx +++ b/webapp/src/components/App/Studies/StudyCard.tsx @@ -162,7 +162,7 @@ const StudyCard = memo((props: Props) => { const handleCopyClick = () => { studyApi - .copyStudy(id, `${study?.name} (${t("study.copyId")})`, false) + .copyStudy(id, `${study?.name} (${t("studies.copySuffix")})`, false) .catch((err) => { enqueueErrorSnackbar(t("studies.error.copyStudy"), err); logError("Failed to copy study", study, err); @@ -440,7 +440,7 @@ const StudyCard = memo((props: Props) => { }} /> - {t("study.copyId")} + {t("global.copy")} {study.managed && ( Date: Fri, 17 Jun 2022 16:12:01 +0200 Subject: [PATCH 06/24] Add study config manager for thematic trimming (#956) --- antarest/study/business/config_management.py | 155 ++++++++++++++++++ antarest/study/service.py | 13 ++ antarest/study/web/study_data_blueprint.py | 52 ++++++ tests/integration/test_integration.py | 9 + tests/storage/business/test_config_manager.py | 105 ++++++++++++ 5 files changed, 334 insertions(+) create mode 100644 antarest/study/business/config_management.py create mode 100644 tests/storage/business/test_config_manager.py diff --git a/antarest/study/business/config_management.py b/antarest/study/business/config_management.py new file mode 100644 index 0000000000..8a5509d1da --- /dev/null +++ b/antarest/study/business/config_management.py @@ -0,0 +1,155 @@ +from enum import Enum +from functools import reduce +from typing import Dict, Any, List, Union + +from antarest.study.business.utils import execute_or_add_commands +from antarest.study.model import ( + RawStudy, + Study, +) +from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.storage.variantstudy.model.command.update_config import ( + UpdateConfig, +) + + +class OutputVariableBase(str, Enum): + OV_COST = "OV. COST" + OP_COST = "OP. COST" + MRG_PRICE = "MRG. PRICE" + CO2_EMIS = "CO2 EMIS." + DTG_BY_PLANT = "DTG by plant" + BALANCE = "BALANCE" + ROW_BAL = "ROW BAL." + PSP = "PSP" + MISC_NDG = "MISC. NDG" + LOAD = "LOAD" + H_ROR = "H. ROR" + WIND = "WIND" + SOLAR = "SOLAR" + NUCLEAR = "NUCLEAR" + LIGNITE = "LIGNITE" + COAL = "COAL" + GAS = "GAS" + OIL = "OIL" + MIX_FUEL = "MIX. FUEL" + MISC_DTG = "MISC. DTG" + H_STOR = "H. STOR" + H_PUMP = "H. PUMP" + H_LEV = "H. LEV" + H_INFL = "H. INFL" + H_OVFL = "H. OVFL" + H_VAL = "H. VAL" + H_COST = "H. COST" + UNSP_ENRG = "UNSP. ENRG" + SPIL_ENRG = "SPIL. ENRG" + LOLD = "LOLD" + LOLP = "LOLP" + AVL_DTG = "AVL DTG" + DTG_MRG = "DTG MRG" + MAX_MRG = "MAX MRG" + NP_COST = "NP COST" + NP_Cost_by_plant = "NP Cost by plant" + NODU = "NODU" + NODU_by_plant = "NODU by plant" + FLOW_LIN = "FLOW LIN." + UCAP_LIN = "UCAP LIN." + LOOP_FLOW = "LOOP FLOW" + FLOW_QUAD = "FLOW QUAD." + CONG_FEE_ALG = "CONG. FEE (ALG.)" + CONG_FEE_ABS = "CONG. FEE (ABS.)" + MARG_COST = "MARG. COST" + CONG_PROD_PLUS = "CONG. PROD +" + CONG_PROD_MINUS = "CONG. PROD -" + HURDLE_COST = "HURDLE COST" + + +class OutputVariable810(str, Enum): + RES_GENERATION_BY_PLANT = "RES generation by plant" + MISC_DTG_2 = "MISC. DTG 2" + MISC_DTG_3 = "MISC. DTG 3" + MISC_DTG_4 = "MISC. DTG 4" + WIND_OFFSHORE = "WIND OFFSHORE" + WIND_ONSHORE = "WIND ONSHORE" + SOLAR_CONCRT = "SOLAR CONCRT." + SOLAR_PV = "SOLAR PV" + SOLAR_ROOFT = "SOLAR ROOFT" + RENW_1 = "RENW. 1" + RENW_2 = "RENW. 2" + RENW_3 = "RENW. 3" + RENW_4 = "RENW. 4" + + +OutputVariable = Union[OutputVariableBase, OutputVariable810] +OUTPUT_VARIABLE_LIST: List[str] = [var.value for var in OutputVariableBase] + [ + var.value for var in OutputVariable810 +] + + +class ConfigManager: + def __init__( + self, + storage_service: StudyStorageService, + ) -> None: + self.storage_service = storage_service + + @staticmethod + def get_output_variables(study: Study) -> List[str]: + version = int(study.version) + if version < 810: + return [var.value for var in OutputVariableBase] + return OUTPUT_VARIABLE_LIST + + def get_thematic_trimming(self, study: Study) -> Dict[str, bool]: + storage_service = self.storage_service.get_storage(study) + file_study = storage_service.get_raw(study) + config = file_study.tree.get(["settings", "generaldata"]) + trimming_config = config.get("variable selection", None) + variable_list = self.get_output_variables(study) + if trimming_config: + if trimming_config.get("selected_vars_reset", True): + return { + var: var not in trimming_config.get("select_var -", []) + for var in variable_list + } + else: + return { + var: var in trimming_config.get("select_var +", []) + for var in variable_list + } + return {var: True for var in variable_list} + + def set_thematic_trimming( + self, study: Study, state: Dict[str, bool] + ) -> None: + file_study = self.storage_service.get_storage(study).get_raw(study) + state_by_active: Dict[bool, List[str]] = reduce( + lambda agg, output: self._agg_states(agg, output, state), + state.keys(), + {True: [], False: []}, + ) + config_data: Dict[str, Any] + if len(state_by_active[True]) > len(state_by_active[False]): + config_data = {"select_var -": state_by_active[False]} + else: + config_data = { + "selected_vars_reset": True, + "select_var +": state_by_active[True], + } + command = UpdateConfig( + target="settings/generaldata/variable selection", + data=config_data, + command_context=self.storage_service.variant_study_service.command_factory.command_context, + ) + execute_or_add_commands( + study, file_study, [command], self.storage_service + ) + + @staticmethod + def _agg_states( + state: Dict[bool, List[str]], + key: str, + ref: Dict[str, bool], + ) -> Dict[bool, List[str]]: + state[ref[key]].append(key) + return state diff --git a/antarest/study/service.py b/antarest/study/service.py index a114a07ff0..2a37c71b04 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -63,6 +63,7 @@ AreaCreationDTO, AreaUI, ) +from antarest.study.business.config_management import ConfigManager from antarest.study.business.link_management import LinkManager, LinkInfoDTO from antarest.study.business.matrix_management import MatrixManager from antarest.study.business.xpansion_management import ( @@ -174,6 +175,7 @@ def __init__( self.task_service = task_service self.areas = AreaManager(self.storage_service, self.repository) self.links = LinkManager(self.storage_service) + self.config_manager = ConfigManager(self.storage_service) self.xpansion_manager = XpansionManager(self.storage_service) self.matrix_manager = MatrixManager(self.storage_service) self.cache_service = cache_service @@ -478,6 +480,17 @@ def update_study_information( ) return new_metadata + def check_study_access( + self, + uuid: str, + permission: StudyPermissionType, + params: RequestParameters, + ) -> Study: + study = self.get_study(uuid) + assert_permission(params.user, study, permission) + self._assert_study_unarchived(study) + return study + def get_study_path(self, uuid: str, params: RequestParameters) -> Path: """ Retrieve study path diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 4aa08e22f3..d3259269bb 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -5,6 +5,7 @@ from antarest.core.config import Config from antarest.core.jwt import JWTUser +from antarest.core.model import StudyPermissionType from antarest.core.requests import ( RequestParameters, ) @@ -21,6 +22,9 @@ AreaInfoDTO, AreaUI, ) +from antarest.study.business.config_management import ( + OutputVariable, +) from antarest.study.business.link_management import LinkInfoDTO from antarest.study.model import PatchCluster, PatchArea from antarest.study.service import StudyService @@ -220,6 +224,54 @@ def edit_matrix( uuid, path, matrix_edit_instructions, params ) + @bp.get( + "/studies/{uuid}/config/thematic_trimming", + tags=[APITag.study_data], + summary="Get thematic trimming config", + response_model=Dict[str, bool], + ) + def get_thematic_trimming( + uuid: str, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> Any: + logger.info( + f"Fetching thematic trimming config for study {uuid}", + extra={"user": current_user.id}, + ) + params = RequestParameters(user=current_user) + study = study_service.check_study_access( + uuid, StudyPermissionType.READ, params + ) + return study_service.config_manager.get_thematic_trimming(study) + + @bp.put( + "/studies/{uuid}/config/thematic_trimming", + tags=[APITag.study_data], + summary="Set thematic trimming config", + ) + def set_thematic_trimming( + uuid: str, + thematic_trimming_config: Dict[OutputVariable, bool] = Body(...), + current_user: JWTUser = Depends(auth.get_current_user), + ) -> Any: + logger.info( + f"Updating thematic trimming config for study {uuid}", + extra={"user": current_user.id}, + ) + params = RequestParameters(user=current_user) + study = study_service.check_study_access( + uuid, StudyPermissionType.WRITE, params + ) + study_service.config_manager.set_thematic_trimming( + study, + { + output_variable.value: thematic_trimming_config[ + output_variable + ] + for output_variable in thematic_trimming_config + }, + ) + @bp.post( "/studies/_update_version", tags=[APITag.study_data], diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 93b5adbe9d..4044ddbd30 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -174,6 +174,15 @@ def test_main(app: FastAPI): ) assert res.status_code == 200 + # config / thematic trimming + res = client.get( + f"/v1/studies/{study_id}/config/thematic_trimming", + headers={ + "Authorization": f'Bearer {george_credentials["access_token"]}' + }, + ) + assert res.status_code == 200 + # study matrix index res = client.get( f"/v1/studies/{study_id}/matrixindex", diff --git a/tests/storage/business/test_config_manager.py b/tests/storage/business/test_config_manager.py new file mode 100644 index 0000000000..88e0833bd1 --- /dev/null +++ b/tests/storage/business/test_config_manager.py @@ -0,0 +1,105 @@ +from pathlib import Path +from unittest.mock import Mock + +from antarest.study.business.config_management import ( + ConfigManager, + OutputVariableBase, + OutputVariable810, + OUTPUT_VARIABLE_LIST, +) +from antarest.study.storage.rawstudy.model.filesystem.config.model import ( + FileStudyTreeConfig, +) +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy +from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import ( + FileStudyTree, +) +from antarest.study.storage.rawstudy.raw_study_service import RawStudyService +from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.storage.variantstudy.model.command.update_config import ( + UpdateConfig, +) +from antarest.study.storage.variantstudy.model.command_context import ( + CommandContext, +) +from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy +from antarest.study.storage.variantstudy.variant_study_service import ( + VariantStudyService, +) + + +def test_thematic_trimming_config(): + command_context = CommandContext.construct() + command_factory_mock = Mock() + command_factory_mock.command_context = command_context + raw_study_service = Mock(spec=RawStudyService) + variant_study_service = Mock( + spec=VariantStudyService, command_factory=command_factory_mock + ) + config_manager = ConfigManager( + storage_service=StudyStorageService( + raw_study_service, variant_study_service + ), + ) + + study = VariantStudy(version="820") + config = FileStudyTreeConfig( + study_path=Path("somepath"), + path=Path("somepath"), + study_id="", + version=-1, + areas={}, + sets={}, + ) + file_tree_mock = Mock(spec=FileStudyTree, context=Mock(), config=config) + variant_study_service.get_raw.return_value = FileStudy( + config=config, tree=file_tree_mock + ) + file_tree_mock.get.side_effect = [ + {}, + {"variable selection": {"select_var -": ["AVL DTG"]}}, + { + "variable selection": { + "selected_vars_reset": False, + "select_var +": ["CONG. FEE (ALG.)"], + } + }, + ] + + expected = {var: True for var in [var for var in OutputVariableBase]} + study.version = "800" + assert config_manager.get_thematic_trimming(study) == expected + + study.version = "820" + expected = {var: True for var in OUTPUT_VARIABLE_LIST} + expected[OutputVariableBase.AVL_DTG] = False + assert config_manager.get_thematic_trimming(study) == expected + expected = {var: False for var in OUTPUT_VARIABLE_LIST} + expected[OutputVariableBase.CONG_FEE_ALG] = True + assert config_manager.get_thematic_trimming(study) == expected + + new_config = {var: True for var in OUTPUT_VARIABLE_LIST} + new_config[OutputVariableBase.COAL] = False + config_manager.set_thematic_trimming(study, new_config) + assert variant_study_service.append_commands.called_with( + UpdateConfig( + target="settings/generaldata/variable selection", + data={"select_var -": [OutputVariableBase.COAL.value]}, + command_context=command_context, + ) + ) + new_config = {var: False for var in OUTPUT_VARIABLE_LIST} + new_config[OutputVariable810.RENW_1] = True + config_manager.set_thematic_trimming(study, new_config) + assert variant_study_service.append_commands.called_with( + UpdateConfig( + target="settings/generaldata/variable selection", + data={ + "selected_vars_reset": False, + "select_var +": [OutputVariable810.RENW_1.value], + }, + command_context=command_context, + ) + ) + + assert len(OUTPUT_VARIABLE_LIST) == 61 From e56a4b4344e12c46db4f4f0c52285781f1a81bfa Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Mon, 20 Jun 2022 13:41:38 +0200 Subject: [PATCH 07/24] Set default lang to en Signed-off-by: Paul Bui-Quang --- webapp/src/i18n.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/i18n.js b/webapp/src/i18n.js index a9e52b1458..38711d972c 100644 --- a/webapp/src/i18n.js +++ b/webapp/src/i18n.js @@ -16,7 +16,7 @@ export function initI18n(version = "unknown") { // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ - fallbackLng: "fr", + fallbackLng: "en", backend: { loadPath: () => `/locales/{{lng}}/{{ns}}.json?v=${version}`, }, From ef8cad95f987dfe4b15e97a9c890dd2653b8b8b8 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Mon, 20 Jun 2022 13:49:38 +0200 Subject: [PATCH 08/24] Fix disappearing favorites by removing favorites update on studies fetch Signed-off-by: Paul Bui-Quang --- webapp/src/redux/ducks/studies.ts | 12 ------------ webapp/src/redux/ducks/studyDataSynthesis.ts | 8 +++++++- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/webapp/src/redux/ducks/studies.ts b/webapp/src/redux/ducks/studies.ts index 776f76e312..24e2a71318 100644 --- a/webapp/src/redux/ducks/studies.ts +++ b/webapp/src/redux/ducks/studies.ts @@ -211,18 +211,6 @@ export const fetchStudies = createAsyncThunk< >(n("FETCH_STUDIES"), async (_, { dispatch, getState, rejectWithValue }) => { try { const studies = await api.getStudies(); - const state = getState(); - const currentFavorites = getFavoriteStudyIds(state); - const newFavorites = R.innerJoin( - (fav, study) => fav === study.id, - currentFavorites, - studies - ); - - if (currentFavorites.length !== newFavorites.length) { - dispatch(setFavoriteStudies(newFavorites)); - } - dispatch(fetchStudyVersions()); return studies; diff --git a/webapp/src/redux/ducks/studyDataSynthesis.ts b/webapp/src/redux/ducks/studyDataSynthesis.ts index 52144c5147..7b49739189 100644 --- a/webapp/src/redux/ducks/studyDataSynthesis.ts +++ b/webapp/src/redux/ducks/studyDataSynthesis.ts @@ -61,13 +61,19 @@ export const createStudyData = createAsyncThunk< // Set current area const areas = Object.keys(studyData.areas); - if (areas.length > 0) dispatch(setCurrentArea(areas[0])); + if (areas.length > 0) { + dispatch(setCurrentArea(areas[0])); + } else { + dispatch(setCurrentArea("")); + } // Set current link const links = selectLinks(studyData); const linkList = links ? Object.values(links) : []; if (linkList.length > 0) { dispatch(setCurrentLink(linkList[0].name)); + } else { + dispatch(setCurrentLink("")); } return studyData; From 213dbc7c29d9caae54228ec947cd0fe119195943 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Tue, 21 Jun 2022 09:31:26 +0200 Subject: [PATCH 09/24] Add commands apply for raw studies (#959) --- antarest/study/business/utils.py | 4 +- antarest/study/main.py | 3 +- antarest/study/service.py | 40 +++++++++++++++++++ .../model/filesystem/matrix/matrix.py | 1 + .../model/filesystem/root/filestudytree.py | 14 +++++++ .../variantstudy/variant_study_service.py | 3 +- antarest/study/web/variant_blueprint.py | 13 +++--- 7 files changed, 68 insertions(+), 10 deletions(-) diff --git a/antarest/study/business/utils.py b/antarest/study/business/utils.py index 7f553b6d91..68c33dd1bb 100644 --- a/antarest/study/business/utils.py +++ b/antarest/study/business/utils.py @@ -6,7 +6,7 @@ from antarest.study.model import Study, RawStudy from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.utils import remove_from_cache +from antarest.study.storage.utils import is_managed from antarest.study.storage.variantstudy.model.command.icommand import ICommand from antarest.study.storage.variantstudy.model.model import CommandDTO @@ -25,6 +25,8 @@ def execute_or_add_commands( raise CommandApplicationError(result.message) executed_commands.append(command) storage_service.variant_study_service.invalidate_cache(study) + if not is_managed(study): + file_study.tree.async_denormalize() else: storage_service.variant_study_service.append_commands( study.id, diff --git a/antarest/study/main.py b/antarest/study/main.py index 50ca9332bf..1ffb38b843 100644 --- a/antarest/study/main.py +++ b/antarest/study/main.py @@ -23,6 +23,7 @@ from antarest.study.storage.rawstudy.raw_study_service import ( RawStudyService, ) +from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.business.matrix_constants_generator import ( GeneratorMatrixConstants, ) @@ -143,7 +144,7 @@ def build_study_service( ) application.include_router( create_study_variant_routes( - variant_study_service=variant_study_service, + study_service=study_service, config=config, ) ) diff --git a/antarest/study/service.py b/antarest/study/service.py index 2a37c71b04..16a888d25a 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -66,6 +66,7 @@ from antarest.study.business.config_management import ConfigManager from antarest.study.business.link_management import LinkManager, LinkInfoDTO from antarest.study.business.matrix_management import MatrixManager +from antarest.study.business.utils import execute_or_add_commands from antarest.study.business.xpansion_management import ( XpansionManager, XpansionSettingsDTO, @@ -139,6 +140,7 @@ UpdateRawFile, ) from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy +from antarest.study.storage.variantstudy.model.model import CommandDTO from antarest.study.storage.variantstudy.variant_study_service import ( VariantStudyService, ) @@ -1415,6 +1417,44 @@ def _edit_study_using_command( raise NotImplementedError() return command # for testing purpose + def apply_commands( + self, uuid: str, commands: List[CommandDTO], params: RequestParameters + ) -> None: + study = self.get_study(uuid) + if isinstance(study, VariantStudy): + self.storage_service.variant_study_service.append_commands( + uuid, commands, params + ) + else: + file_study = self.storage_service.raw_study_service.get_raw(study) + assert_permission(params.user, study, StudyPermissionType.WRITE) + self._assert_study_unarchived(study) + parsed_commands: List[ICommand] = [] + for command in commands: + parsed_commands.extend( + self.storage_service.variant_study_service.command_factory.to_icommand( + command + ) + ) + execute_or_add_commands( + study, + file_study, + parsed_commands, + self.storage_service, + ) + self.event_bus.push( + Event( + type=EventType.STUDY_DATA_EDITED, + payload=study.to_json_summary(), + permissions=create_permission_from_study(study), + ) + ) + logger.info( + "Study %s updated by user %s", + uuid, + params.get_user_id(), + ) + def edit_study( self, uuid: str, diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py index d9c509ed51..3bb3ba144e 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py @@ -53,6 +53,7 @@ def denormalize(self) -> None: if self.config.path.exists() or not self.get_link_path().exists(): return + logger.info(f"Denormalizing matrix {self.config.path}") uuid = self.get_link_path().read_text() matrix = self.context.resolver.resolve(uuid) if not matrix or not isinstance(matrix, dict): diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py b/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py index bb0e56e154..8785f02050 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py @@ -1,3 +1,6 @@ +import logging +from threading import Thread + from antarest.study.storage.rawstudy.model.filesystem.folder_node import ( FolderNode, ) @@ -26,6 +29,9 @@ ) +logger = logging.getLogger(__name__) + + class FileStudyTree(FolderNode): """ Top level node of antares tree structure @@ -54,3 +60,11 @@ def build(self) -> TREE: children["output"] = Output(self.context, output_config) return children + + def async_denormalize(self) -> Thread: + logger.info( + f"Denormalizing (async) study data for study {self.config.study_id}" + ) + thread = Thread(target=self.denormalize) + thread.start() + return thread diff --git a/antarest/study/storage/variantstudy/variant_study_service.py b/antarest/study/storage/variantstudy/variant_study_service.py index 04cc8ff2bd..8acf75c9d8 100644 --- a/antarest/study/storage/variantstudy/variant_study_service.py +++ b/antarest/study/storage/variantstudy/variant_study_service.py @@ -221,7 +221,7 @@ def append_commands( study_id: str, commands: List[CommandDTO], params: RequestParameters, - ) -> str: + ) -> None: """ Add command to list of commands (at the end) Args: @@ -245,7 +245,6 @@ def append_commands( ] ) self.invalidate_cache(study) - return str(study.id) def replace_commands( self, diff --git a/antarest/study/web/variant_blueprint.py b/antarest/study/web/variant_blueprint.py index dfd3b0775a..cb34fc89a6 100644 --- a/antarest/study/web/variant_blueprint.py +++ b/antarest/study/web/variant_blueprint.py @@ -13,6 +13,8 @@ from antarest.core.utils.web import APITag from antarest.login.auth import Auth from antarest.study.model import StudyMetadataDTO +from antarest.study.service import StudyService +from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.model import ( CommandDTO, VariantTreeDTO, @@ -25,13 +27,13 @@ def create_study_variant_routes( - variant_study_service: VariantStudyService, + study_service: StudyService, config: Config, ) -> APIRouter: """ Endpoint implementation for studies area management Args: - variant_study_service: study service facade to handle request + study_service: study service facade to handle request config: main server configuration Returns: @@ -39,6 +41,7 @@ def create_study_variant_routes( """ bp = APIRouter(prefix="/v1") auth = Auth(config) + variant_study_service = study_service.storage_service.variant_study_service @bp.post( "/studies/{uuid}/variants", @@ -159,16 +162,14 @@ def append_commands( uuid: str, commands: List[CommandDTO] = Body(...), current_user: JWTUser = Depends(auth.get_current_user), - ) -> str: + ) -> None: logger.info( f"Appending new command to variant study {uuid}", extra={"user": current_user.id}, ) params = RequestParameters(user=current_user) sanitized_uuid = sanitize_uuid(uuid) - return variant_study_service.append_commands( - sanitized_uuid, commands, params - ) + study_service.apply_commands(sanitized_uuid, commands, params) @bp.put( "/studies/{uuid}/commands", From 4da9b4dc22567c643bac5e292977c09ea79eea2f Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Tue, 21 Jun 2022 11:40:39 +0200 Subject: [PATCH 10/24] Add docker-compose prod deployment template (#952) --- Dockerfile | 3 +- README.md | 8 +- .../9846e90c2868_fix_bot_foreign_key.py | 2 +- antarest/study/storage/rawstudy/watcher.py | 7 +- .../business/matrix_constants_generator.py | 6 +- docker-compose.override.yml.example | 10 +++ docker-compose.yml | 65 ++++++++++++++ docs/assets/media/img/readme_screenshot.png | Bin 0 -> 319788 bytes docs/install/0-INSTALL.md | 18 ++-- docs/install/2-DEPLOY.md | 66 +++++++++++++-- resources/deploy/config.prod.yaml | 80 ++++++++++++++++++ resources/deploy/config.yaml | 7 +- resources/deploy/gunicorn.py | 30 +++++++ resources/deploy/logs/.placeholder | 0 resources/deploy/nginx.conf | 24 ++++++ resources/deploy/web.config.json | 4 + 16 files changed, 306 insertions(+), 24 deletions(-) create mode 100644 docker-compose.override.yml.example create mode 100644 docker-compose.yml create mode 100644 docs/assets/media/img/readme_screenshot.png create mode 100644 resources/deploy/config.prod.yaml create mode 100644 resources/deploy/gunicorn.py create mode 100644 resources/deploy/logs/.placeholder create mode 100644 resources/deploy/nginx.conf create mode 100644 resources/deploy/web.config.json diff --git a/Dockerfile b/Dockerfile index ee2825af17..dccc2ff4fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM python:3.8-slim-buster +#FROM python:3.8-slim-buster +FROM brunneis/python:3.8.3-ubuntu-20.04 ENV ANTAREST_CONF /resources/application.yaml diff --git a/README.md b/README.md index 3d6df51e0c..a9a3db1298 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ -# AntaREST Storage +# Antares Web [![CI](https://github.com/AntaresSimulatorTeam/AntaREST/workflows/main/badge.svg)](https://github.com/AntaresSimulatorTeam/AntaREST/actions?query=workflow%3Amain) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=AntaresSimulatorTeam_api-iso-antares&metric=coverage)](https://sonarcloud.io/dashboard?id=AntaresSimulatorTeam_api-iso-antares) [![Licence](https://img.shields.io/github/license/AntaresSimulatorTeam/AntaREST)](https://www.apache.org/licenses/LICENSE-2.0) - +![Screenshot](./docs/assets/media/img/readme_screenshot.png) + +## Documentation + +The full project documentation can be found in the [readthedocs website](https://antares-web.readthedocs.io/en/latest). ## Build the API diff --git a/alembic/versions/9846e90c2868_fix_bot_foreign_key.py b/alembic/versions/9846e90c2868_fix_bot_foreign_key.py index 9fa93fc913..c79b428405 100644 --- a/alembic/versions/9846e90c2868_fix_bot_foreign_key.py +++ b/alembic/versions/9846e90c2868_fix_bot_foreign_key.py @@ -5,7 +5,6 @@ Create Date: 2021-11-19 11:58:11.378519 """ -from sqlite3 import Connection from alembic import op import sqlalchemy as sa @@ -13,6 +12,7 @@ # revision identifiers, used by Alembic. from sqlalchemy import text +from sqlalchemy.engine import Connection from antarest.login.model import Bot, User diff --git a/antarest/study/storage/rawstudy/watcher.py b/antarest/study/storage/rawstudy/watcher.py index 3f6539f093..0648b0fd15 100644 --- a/antarest/study/storage/rawstudy/watcher.py +++ b/antarest/study/storage/rawstudy/watcher.py @@ -1,5 +1,6 @@ import logging import re +import tempfile import threading from html import escape from http import HTTPStatus @@ -33,8 +34,8 @@ class Watcher: Files Watcher to listen raw studies changes and trigger a database update. """ - LOCK = Path("watcher") - SCAN_LOCK = Path("scan.lock") + LOCK = Path(tempfile.gettempdir()) / "watcher" + SCAN_LOCK = Path(tempfile.gettempdir()) / "scan.lock" def __init__( self, @@ -256,7 +257,7 @@ def scan( logger.info( f"Waiting for FileLock to synchronize {directory_path or 'all studies'}" ) - with FileLock(self.config.storage.tmp_dir / Watcher.SCAN_LOCK): + with FileLock(Watcher.SCAN_LOCK): logger.info( f"FileLock acquired to synchronize for {directory_path or 'all studies'}" ) diff --git a/antarest/study/storage/variantstudy/business/matrix_constants_generator.py b/antarest/study/storage/variantstudy/business/matrix_constants_generator.py index dd47538dd2..6069acebb8 100644 --- a/antarest/study/storage/variantstudy/business/matrix_constants_generator.py +++ b/antarest/study/storage/variantstudy/business/matrix_constants_generator.py @@ -1,3 +1,5 @@ +import tempfile +from pathlib import Path from typing import Dict from filelock import FileLock @@ -32,7 +34,9 @@ class GeneratorMatrixConstants: def __init__(self, matrix_service: ISimpleMatrixService) -> None: self.hashes: Dict[str, str] = {} self.matrix_service: ISimpleMatrixService = matrix_service - with FileLock("matrix_constant_init.lock"): + with FileLock( + str(Path(tempfile.gettempdir()) / "matrix_constant_init.lock") + ): self._init() def _init(self) -> None: diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example new file mode 100644 index 0000000000..478988454a --- /dev/null +++ b/docker-compose.override.yml.example @@ -0,0 +1,10 @@ +version: '2.1' +services: + antares-antarest: + user: UID:GID + antares-antarest-watcher: + user: UID:GID + antares-antarest-matrix-gc: + user: UID:GID + postgresql: + user: UID:GID diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..a18e3bc817 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +version: '2.1' +services: + antares-antarest: + image: antarest:latest + container_name : antarest + environment : + - UVICORN_ROOT_PATH=/api + depends_on: + - redis + - postgresql + volumes: + - ./resources/deploy/examples:/workspaces + - ./resources/deploy/tmp:/antarest_tmp_dir + - ./resources/deploy/matrices:/matrixstore + - ./resources/deploy/config.prod.yaml:/resources/application.yaml + - ./resources/deploy/logs:/logs + - ./resources/deploy/gunicorn.py:/conf/gunicorn.py + - ./antares-8.2.2-Ubuntu-20.04/bin:/antares_simulator + antares-antarest-watcher: + image: antarest:latest + container_name: antarest-watcher + volumes: + - ./resources/deploy/examples:/workspaces + - ./resources/deploy/tmp:/antarest_tmp_dir + - ./resources/deploy/matrices:/matrixstore + - ./resources/deploy/config.prod.yaml:/resources/application.yaml + - ./resources/deploy/logs:/logs + depends_on: + - antares-antarest + command: watcher + antares-antarest-matrix-gc: + image: antarest:latest + container_name : antarest-matrix-gc + volumes: + - ./resources/deploy/examples:/workspaces + - ./resources/deploy/tmp:/antarest_tmp_dir + - ./resources/deploy/matrices:/matrixstore + - ./resources/deploy/config.prod.yaml:/resources/application.yaml + - ./resources/deploy/logs:/logs + depends_on: + - antares-antarest + command: matrix_gc + postgresql: + image: postgres:latest + container_name: postgres + environment: + - POSTGRES_PASSWORD=somepass + - PG_DATA=/var/lib/postgresql/data/pgdata + volumes: + - ./resources/deploy/db:/var/lib/postgresql/data + command: [ "postgres", "-c", "log_statement=all", "-c", "log_destination=stderr" ] + redis: + image: redis:latest + container_name : redis + nginx: + image: nginx:latest + container_name: nginx + depends_on: + - antares-antarest + ports: + - 80:80 + volumes: + - ./resources/deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./webapp/build:/www + - ./resources/deploy/web.config.json:/www/config.json:ro \ No newline at end of file diff --git a/docs/assets/media/img/readme_screenshot.png b/docs/assets/media/img/readme_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c555306ecd60856af239c3b3096056065f750d5f GIT binary patch literal 319788 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ƥxk`9YYQbCdCW{zKP`TWPoMKT6wgRauhFwF!~ zR!Ns=i04o8%D3Zp+r&x(aY=g#u2M^?iwlRge8aa)64;HkE?O(yJesgm*4`8l>()_LL3P>dccKIkE` zBr0`bN}fN<2sDtBmvk$(3d<N0U5bHA!(v@`T$8@u(6twk3v7=E;bCwyUl7#ORDL;mhkF}daNY8ccyOm zzm+2*iy|)U>k4_}c|Qa0O-;#lm5?n=g>42IBYpi+sWt*Uq>Cv-VBM-eH%C}CSWyw= zNNCXGdAO~1BL)XzD#GGWaSsu3!(0)eDbh7XP^?Gs{^b-&?$&a{@FC} zVS66WQ7BSpZqD#?gO3V(wL<&kntNlvd*6LG@Tqq^l3iP)%9ES%e&b^rN1*9nT*_s8 z?r5fJ(K=VYyA6mY7Osk9=U=*(-RgVEzNBkH`1}F)FnDpz_zgWnDaLTx4M}H{Wy~BA zfgZKT0zN$f=-U~zbp@KYGtAbL10Z`HcnbP+dNPDfWLdBj;&%p@x72@odS~QpP^eSe z*&1?8{a6uso&2UH=(9ZzV;D%uA`%|0o-P^bfW{G-b2C}{bz0|vf>59?W6f+*`Q25t zf(5KRMY(>tm6J=k^ELpb)Abo4;0AlcU`H@^?CK;IH)VBvL>>vTi)a8EpNfOpw^x^i!uGn5O)A`uR6?C13k@#l7>JblTp>B9baO`Ls;DVH$ zXyMh7ZkMCgb&PO>th>`@b&M_bI6xdfxOEx>tcqsa@KY`DsdUuj5^lN)7uy5jR z`!n)#f*|P7NiXQuZi*=z02n-M*2BwY9~g&N9dXiso5nuA3oS^KHo))}2QDf+uqP+OJuYag-ziB@1dVyfm>HS}K@S2H0rl?t6LxrbG zp)h9Ytjx_Izg|{^4l2(gmEa)P*56{nq{f8{ggChr7-&K-7ig4W6z_H_4GM zo)|Ztj62g&5UOtF)^5{dD4|T;yn3$S;3nE4lbx*|@pz8@*?c4;aAh3S;4flvD2hp4Vdm7L%#Memm9rVOR2&9zYIB_Or;TW9n1@VbR3L5J<}S zd3svcC-jjnz}^TeX)YtH5sG!s%I~51&^+bsjR`h9Q+raSuo>G0Ky!gQ-&p}$9YLKZ z3B#W+5p9wuhc8zz@5lQ<;;s*≺NYpAWA9GlErHnpTZ)M6?2<*U4n+-1XNC1nf?l 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

VHi;^N!nFN>d_@%Ct_U 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 z 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

6Y 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�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∼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%0As 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 JF6*2UngC^ Date: Tue, 21 Jun 2022 17:41:48 +0200 Subject: [PATCH 11/24] Fix async denormalizing Signed-off-by: Paul Bui-Quang --- .../rawstudy/model/filesystem/root/filestudytree.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py b/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py index 8785f02050..25a30908a1 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py @@ -1,6 +1,7 @@ import logging from threading import Thread +from antarest.core.utils.fastapi_sqlalchemy import db from antarest.study.storage.rawstudy.model.filesystem.folder_node import ( FolderNode, ) @@ -65,6 +66,10 @@ def async_denormalize(self) -> Thread: logger.info( f"Denormalizing (async) study data for study {self.config.study_id}" ) - thread = Thread(target=self.denormalize) + thread = Thread(target=self._threaded_denormalize) thread.start() return thread + + def _threaded_denormalize(self): + with db(): + self.denormalize() From 60e9cae4f2fe7b12489c9ae96c5ffd2638c8a137 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Tue, 21 Jun 2022 17:43:27 +0200 Subject: [PATCH 12/24] Add link UI + add study listing search filters (#962) --- antarest/study/business/link_management.py | 29 +++++++++++++-- antarest/study/service.py | 17 +++++++-- antarest/study/storage/utils.py | 19 +++++++++- antarest/study/web/studies_blueprint.py | 5 ++- antarest/study/web/study_data_blueprint.py | 3 +- tests/integration/test_integration.py | 10 ++++- .../storage/business/test_arealink_manager.py | 15 ++------ tests/storage/test_service.py | 37 ++++++++++++++++++- 8 files changed, 111 insertions(+), 24 deletions(-) diff --git a/antarest/study/business/link_management.py b/antarest/study/business/link_management.py index 559142fd4c..a4d20db1a8 100644 --- a/antarest/study/business/link_management.py +++ b/antarest/study/business/link_management.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional, Dict, Any from pydantic import BaseModel @@ -13,21 +13,44 @@ ) +class LinkUIDTO(BaseModel): + color: str + width: float + style: str + + class LinkInfoDTO(BaseModel): area1: str area2: str + ui: Optional[LinkUIDTO] = None class LinkManager: def __init__(self, storage_service: StudyStorageService) -> None: self.storage_service = storage_service - def get_all_links(self, study: Study) -> List[LinkInfoDTO]: + def get_all_links( + self, study: Study, with_ui: bool = False + ) -> List[LinkInfoDTO]: file_study = self.storage_service.get_storage(study).get_raw(study) result = [] for area_id, area in file_study.config.areas.items(): + links_config: Optional[Dict[str, Any]] = None + if with_ui: + links_config = file_study.tree.get( + ["input", "links", area_id, "properties"] + ) for link in area.links: - result.append(LinkInfoDTO(area1=area_id, area2=link)) + ui_info: Optional[LinkUIDTO] = None + if with_ui and links_config and link in links_config: + ui_info = LinkUIDTO( + color=f"{links_config[link]['colorr']},{links_config[link]['colorg']},{links_config[link]['colorb']}", + width=links_config[link]["link-width"], + style=links_config[link]["link-style"], + ) + result.append( + LinkInfoDTO(area1=area_id, area2=link, ui=ui_info) + ) return result diff --git a/antarest/study/service.py b/antarest/study/service.py index 16a888d25a..67419f6e79 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -125,6 +125,7 @@ assert_permission, create_permission_from_study, get_start_date, + study_matcher, ) from antarest.study.storage.variantstudy.model.command.icommand import ICommand from antarest.study.storage.variantstudy.model.command.replace_matrix import ( @@ -316,12 +317,20 @@ def _get_study_metadatas(self, params: RequestParameters) -> List[Study]: ) def get_studies_information( - self, managed: bool, params: RequestParameters + self, + managed: bool, + name: Optional[str], + workspace: Optional[str], + folder: Optional[str], + params: RequestParameters, ) -> Dict[str, StudyMetadataDTO]: """ Get information for all studies. Args: managed: indicate if just managed studies should be retrieved + name: optional name of the study to match + folder: optional folder prefix of the study to match + workspace: optional workspace of the study to match params: request parameters Returns: List of study information @@ -356,7 +365,8 @@ def get_studies_information( study_dto, StudyPermissionType.READ, raising=False, - ), + ) + and study_matcher(name, workspace, folder), studies.values(), ) } @@ -1668,12 +1678,13 @@ def get_all_areas( def get_all_links( self, uuid: str, + with_ui: bool, params: RequestParameters, ) -> List[LinkInfoDTO]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) self._assert_study_unarchived(study) - return self.links.get_all_links(study) + return self.links.get_all_links(study, with_ui) def create_area( self, diff --git a/antarest/study/storage/utils.py b/antarest/study/storage/utils.py index 8159ac6bc3..a1a3299dfd 100644 --- a/antarest/study/storage/utils.py +++ b/antarest/study/storage/utils.py @@ -7,7 +7,7 @@ from math import ceil from pathlib import Path from time import strptime -from typing import Optional, Union, cast +from typing import Optional, Union, cast, List, Callable from uuid import uuid4 from zipfile import ZipFile @@ -214,6 +214,23 @@ def create_permission_from_study( ) +def study_matcher( + name: Optional[str], workspace: Optional[str], folder: Optional[str] +) -> Callable[[StudyMetadataDTO], bool]: + def study_match(study: StudyMetadataDTO) -> bool: + if name and not study.name.startswith(name): + return False + if workspace and study.workspace != workspace: + return False + if folder and ( + not study.folder or not study.folder.startswith(folder) + ): + return False + return True + + return study_match + + def assert_permission( user: Optional[JWTUser], study: Optional[Union[Study, StudyMetadataDTO]], diff --git a/antarest/study/web/studies_blueprint.py b/antarest/study/web/studies_blueprint.py index 0750ccc36a..535e4fbd28 100644 --- a/antarest/study/web/studies_blueprint.py +++ b/antarest/study/web/studies_blueprint.py @@ -61,12 +61,15 @@ def create_study_routes( ) def get_studies( managed: bool = False, + name: Optional[str] = None, + folder: Optional[str] = None, + workspace: Optional[str] = None, current_user: JWTUser = Depends(auth.get_current_user), ) -> Any: logger.info(f"Fetching study list", extra={"user": current_user.id}) params = RequestParameters(user=current_user) available_studies = study_service.get_studies_information( - managed, params + managed, name, workspace, folder, params ) return available_studies diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index d3259269bb..27246e3a2f 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -75,6 +75,7 @@ def get_areas( ) def get_links( uuid: str, + with_ui: bool = False, current_user: JWTUser = Depends(auth.get_current_user), ) -> Any: logger.info( @@ -82,7 +83,7 @@ def get_links( extra={"user": current_user.id}, ) params = RequestParameters(user=current_user) - areas_list = study_service.get_all_links(uuid, params) + areas_list = study_service.get_all_links(uuid, with_ui, params) return areas_list @bp.post( diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 4044ddbd30..f4865ba097 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -609,12 +609,18 @@ def test_area_management(app: FastAPI): }, ) res_links = client.get( - f"/v1/studies/{study_id}/links", + f"/v1/studies/{study_id}/links?with_ui=true", headers={ "Authorization": f'Bearer {admin_credentials["access_token"]}' }, ) - assert res_links.json() == [{"area1": "area 1", "area2": "area 2"}] + assert res_links.json() == [ + { + "area1": "area 1", + "area2": "area 2", + "ui": {"color": "112,112,112", "style": "plain", "width": 1.0}, + } + ] client.delete( f"/v1/studies/{study_id}/links/area%201/area%202", headers={ diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index fa33b6e02a..6d05bfc525 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -410,18 +410,9 @@ def test_get_all_area(): links = link_manager.get_all_links(study) assert [ - { - "area1": "a1", - "area2": "a2", - }, - { - "area1": "a1", - "area2": "a3", - }, - { - "area1": "a2", - "area2": "a3", - }, + {"area1": "a1", "area2": "a2", "ui": None}, + {"area1": "a1", "area2": "a3", "ui": None}, + {"area1": "a2", "area2": "a3", "ui": None}, ] == [link.dict() for link in links] pass diff --git a/tests/storage/test_service.py b/tests/storage/test_service.py index c867215d38..b06d0f3824 100644 --- a/tests/storage/test_service.py +++ b/tests/storage/test_service.py @@ -67,7 +67,7 @@ RawFileNode, ) from antarest.study.storage.rawstudy.raw_study_service import RawStudyService -from antarest.study.storage.utils import assert_permission +from antarest.study.storage.utils import assert_permission, study_matcher from antarest.study.storage.variantstudy.business.matrix_constants_generator import ( GeneratorMatrixConstants, ) @@ -218,6 +218,9 @@ def test_study_listing() -> None: studies = service.get_studies_information( managed=False, + name=None, + workspace=None, + folder=None, params=RequestParameters( user=JWTUser(id=2, impersonator=2, type="users") ), @@ -231,6 +234,9 @@ def test_study_listing() -> None: studies = service.get_studies_information( managed=False, + name=None, + workspace=None, + folder=None, params=RequestParameters( user=JWTUser(id=2, impersonator=2, type="users") ), @@ -242,6 +248,9 @@ def test_study_listing() -> None: cache.get.return_value = None studies = service.get_studies_information( managed=True, + name=None, + workspace=None, + folder=None, params=RequestParameters( user=JWTUser(id=2, impersonator=2, type="users") ), @@ -934,6 +943,32 @@ def test_check_errors(): repo.get.assert_called_once_with("hello world") +@pytest.mark.unit_test +def test_study_match() -> None: + assert not study_matcher(name=None, folder="ab", workspace="hell")( + StudyMetadataDTO.construct(id="1", folder="abc/de", workspace="hello") + ) + assert study_matcher(name=None, folder="ab", workspace="hello")( + StudyMetadataDTO.construct(id="1", folder="abc/de", workspace="hello") + ) + assert not study_matcher(name=None, folder="abd", workspace="hello")( + StudyMetadataDTO.construct(id="1", folder="abc/de", workspace="hello") + ) + assert not study_matcher(name=None, folder="ab", workspace="hello")( + StudyMetadataDTO.construct(id="1", workspace="hello") + ) + assert study_matcher(name="f", folder=None, workspace="hello")( + StudyMetadataDTO.construct( + id="1", name="foo", folder="abc/de", workspace="hello" + ) + ) + assert not study_matcher(name="foob", folder=None, workspace="hell")( + StudyMetadataDTO.construct( + id="1", name="foo", folder="abc/de", workspace="hello" + ) + ) + + @pytest.mark.unit_test def test_assert_permission() -> None: uuid = str(uuid4()) From b33a7577f5acbd23f73978504745daf8826e9342 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Tue, 21 Jun 2022 17:48:22 +0200 Subject: [PATCH 13/24] Fix mypy Signed-off-by: Paul Bui-Quang --- .../storage/rawstudy/model/filesystem/root/filestudytree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py b/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py index 25a30908a1..39b38c0ea7 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py @@ -70,6 +70,6 @@ def async_denormalize(self) -> Thread: thread.start() return thread - def _threaded_denormalize(self): + def _threaded_denormalize(self) -> None: with db(): self.denormalize() From ca5c2b7a1b720eb1d73e14bb677b23d994d2b385 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Wed, 22 Jun 2022 18:50:02 +0200 Subject: [PATCH 14/24] Fix check_state slurm_launcher loop stop condition (#966) --- .../launcher/adapters/slurm_launcher/slurm_launcher.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py b/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py index da123b103f..71c3b1e513 100644 --- a/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py +++ b/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py @@ -310,10 +310,10 @@ def _check_studies_state(self) -> None: study_list = self.data_repo_tinydb.get_list_of_studies() - all_done = True + nb_study_done = 0 for study in study_list: - all_done = all_done and (study.finished or study.with_error) + nb_study_done += 1 if (study.finished or study.with_error) else 0 if study.done: try: self.log_tail_manager.stop_tracking( @@ -355,7 +355,8 @@ def _check_studies_state(self) -> None: self.create_update_log(study.name), ) - if all_done: + # we refetch study list here because by the time the import_output is done, maybe some new studies has been added + if nb_study_done == len(self.data_repo_tinydb.get_list_of_studies()): self.stop() @staticmethod From 71de7ff91aa0c017a7d442e891ce4e2412fc438a Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Wed, 22 Jun 2022 19:18:31 +0200 Subject: [PATCH 15/24] Fix current area/link selection and studydata update (#965) --- webapp/src/redux/ducks/studies.ts | 16 ++++- webapp/src/redux/ducks/studyDataSynthesis.ts | 67 ++++++++++++++------ webapp/src/redux/selectors.ts | 2 + webapp/src/services/webSockets.ts | 4 +- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/webapp/src/redux/ducks/studies.ts b/webapp/src/redux/ducks/studies.ts index 24e2a71318..8f47aec59a 100644 --- a/webapp/src/redux/ducks/studies.ts +++ b/webapp/src/redux/ducks/studies.ts @@ -18,7 +18,13 @@ import { import * as api from "../../services/api/study"; import { getFavoriteStudyIds, getStudyVersions } from "../selectors"; import { AppAsyncThunkConfig, AppThunk } from "../store"; -import { makeActionName, FetchStatus, AsyncEntityState } from "../utils"; +import { + makeActionName, + FetchStatus, + AsyncEntityState, + createThunk, +} from "../utils"; +import { setDefaultAreaLinkSelection } from "./studyDataSynthesis"; const studiesAdapter = createEntityAdapter(); @@ -93,9 +99,13 @@ const n = makeActionName("study"); // Action Creators //////////////////////////////////////////////////////////////// -export const setCurrentStudy = createAction< +export const setCurrentStudy = createThunk< + NonNullable, NonNullable ->(n("SET_CURRENT")); +>(n("SET_CURRENT"), (arg: string, { dispatch }) => { + dispatch(setDefaultAreaLinkSelection(arg)); + return arg; +}); export const setStudyScrollPosition = createAction< StudiesState["scrollPosition"] diff --git a/webapp/src/redux/ducks/studyDataSynthesis.ts b/webapp/src/redux/ducks/studyDataSynthesis.ts index 7b49739189..e3222eaec0 100644 --- a/webapp/src/redux/ducks/studyDataSynthesis.ts +++ b/webapp/src/redux/ducks/studyDataSynthesis.ts @@ -11,8 +11,8 @@ import { WSMessage, } from "../../common/types"; import * as api from "../../services/api/study"; -import { selectLinks } from "../selectors"; -import { AppAsyncThunkConfig } from "../store"; +import { getStudyData, getStudyDataIds, selectLinks } from "../selectors"; +import { AppAsyncThunkConfig, AppDispatch, AppThunk } from "../store"; import { makeActionName } from "../utils"; export const studyDataAdapter = createEntityAdapter({ @@ -48,6 +48,41 @@ export const setCurrentLink = createAction< // Thunks //////////////////////////////////////////////////////////////// +const initDefaultAreaLinkSelection = ( + dispatch: AppDispatch, + studyData?: FileStudyTreeConfigDTO +): void => { + if (studyData) { + // Set current area + const areas = Object.keys(studyData.areas); + if (areas.length > 0) { + dispatch(setCurrentArea(areas[0])); + } else { + dispatch(setCurrentArea("")); + } + + // Set current link + const links = selectLinks(studyData); + const linkList = links ? Object.values(links) : []; + if (linkList.length > 0) { + dispatch(setCurrentLink(linkList[0].name)); + } else { + dispatch(setCurrentLink("")); + } + } else { + dispatch(setCurrentArea("")); + dispatch(setCurrentLink("")); + } +}; + +export const setDefaultAreaLinkSelection = + (studyId: FileStudyTreeConfigDTO["study_id"]): AppThunk => + (dispatch, getState) => { + const state = getState(); + const studyData = getStudyData(state, studyId); + initDefaultAreaLinkSelection(dispatch, studyData); + }; + export const createStudyData = createAsyncThunk< FileStudyTreeConfigDTO, FileStudyTreeConfigDTO["study_id"], @@ -58,24 +93,7 @@ export const createStudyData = createAsyncThunk< try { // Fetch study synthesis data const studyData = await api.getStudySynthesis(studyId); - - // Set current area - const areas = Object.keys(studyData.areas); - if (areas.length > 0) { - dispatch(setCurrentArea(areas[0])); - } else { - dispatch(setCurrentArea("")); - } - - // Set current link - const links = selectLinks(studyData); - const linkList = links ? Object.values(links) : []; - if (linkList.length > 0) { - dispatch(setCurrentLink(linkList[0].name)); - } else { - dispatch(setCurrentLink("")); - } - + initDefaultAreaLinkSelection(dispatch, studyData); return studyData; } catch (err) { return rejectWithValue(err); @@ -92,6 +110,15 @@ export const setStudyData = createAsyncThunk< return api.getStudySynthesis(id as string).catch(rejectWithValue); }); +export const refreshStudyData = + (event: WSMessage): AppThunk => + (dispatch, getState) => { + const state = getState(); + if (getStudyDataIds(state).indexOf(event.payload.id) !== -1) { + dispatch(setStudyData(event)); + } + }; + export const deleteStudyData = createAsyncThunk< FileStudyTreeConfigDTO["study_id"], FileStudyTreeConfigDTO["study_id"] | WSMessage, diff --git a/webapp/src/redux/selectors.ts b/webapp/src/redux/selectors.ts index afb4b6cdf1..2ab92a005b 100644 --- a/webapp/src/redux/selectors.ts +++ b/webapp/src/redux/selectors.ts @@ -176,6 +176,8 @@ export const getStudyDataState = (state: AppState): StudyDataState => const studyDataSelectors = studyDataAdapter.getSelectors(getStudyDataState); +export const getStudyDataIds = studyDataSelectors.selectIds; + export const getAllStudyData = studyDataSelectors.selectAll; export const getStudyData = studyDataSelectors.selectById; diff --git a/webapp/src/services/webSockets.ts b/webapp/src/services/webSockets.ts index 9f02142054..db0c23e5d4 100644 --- a/webapp/src/services/webSockets.ts +++ b/webapp/src/services/webSockets.ts @@ -17,7 +17,7 @@ import { setMessageInfo, setWebSocketConnected, } from "../redux/ducks/ui"; -import { setStudyData } from "../redux/ducks/studyDataSynthesis"; +import { refreshStudyData } from "../redux/ducks/studyDataSynthesis"; const logInfo = debug("antares:websocket:info"); const logError = debug("antares:websocket:error"); @@ -206,7 +206,7 @@ function makeStudyDataListener(dispatch: AppDispatch) { return function listener(e: WSMessage): void { switch (e.type) { case WSEvent.STUDY_DATA_EDITED: - dispatch(setStudyData(e)); + dispatch(refreshStudyData(e)); break; } }; From 8551fd721e251aecb2b5d6fce49d07d51248def4 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Mon, 27 Jun 2022 14:58:35 +0200 Subject: [PATCH 16/24] Issue 850 Configuration General params Thematic trimming (#967) --- .../model/filesystem/ini_file_node.py | 37 ++-- webapp/package.json | 1 + webapp/src/common/types.ts | 65 +++++- .../Configuration/General/Fields/index.tsx | 30 ++- .../dialogs/ThematicTrimmingDialog/index.tsx | 122 +++++++++++ .../dialogs/ThematicTrimmingDialog/utils.ts | 196 ++++++++++++++++++ .../explore/Configuration/General/index.tsx | 29 ++- .../explore/Configuration/General/utils.ts | 17 +- webapp/src/components/common/Form/index.tsx | 92 +++++--- .../common/fieldEditors/SelectFE.tsx | 7 +- .../common/fieldEditors/SwitchFE.tsx | 4 + webapp/src/hooks/useAutoUpdateRef.tsx | 13 ++ webapp/src/services/api/study.ts | 17 +- webapp/src/theme.ts | 2 - 14 files changed, 570 insertions(+), 62 deletions(-) create mode 100644 webapp/src/components/App/Singlestudy/explore/Configuration/General/dialogs/ThematicTrimmingDialog/index.tsx create mode 100644 webapp/src/components/App/Singlestudy/explore/Configuration/General/dialogs/ThematicTrimmingDialog/utils.ts create mode 100644 webapp/src/hooks/useAutoUpdateRef.tsx diff --git a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py index 10411feced..35784fcaaf 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py @@ -1,5 +1,10 @@ +import os +import tempfile +from pathlib import Path from typing import List, Optional, cast, Dict, Any, Union +from filelock import FileLock + from antarest.core.model import JSON, SUB_JSON from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.io.reader import IniReader @@ -93,19 +98,25 @@ def get_node( def save(self, data: SUB_JSON, url: Optional[List[str]] = None) -> None: url = url or [] - json = self.reader.read(self.path) if self.path.exists() else {} - formatted_data = data - if isinstance(data, str): - formatted_data = IniReader.parse_value(data) - if len(url) == 2: - if url[0] not in json: - json[url[0]] = {} - json[url[0]][url[1]] = formatted_data - elif len(url) == 1: - json[url[0]] = formatted_data - else: - json = cast(JSON, formatted_data) - self.writer.write(json, self.path) + with FileLock( + str( + Path(tempfile.gettempdir()) + / f"{self.config.study_id}-{self.path.relative_to(self.config.study_path).name.replace(os.sep, '.')}.lock" + ) + ): + json = self.reader.read(self.path) if self.path.exists() else {} + formatted_data = data + if isinstance(data, str): + formatted_data = IniReader.parse_value(data) + if len(url) == 2: + if url[0] not in json: + json[url[0]] = {} + json[url[0]][url[1]] = formatted_data + elif len(url) == 1: + json[url[0]] = formatted_data + else: + json = cast(JSON, formatted_data) + self.writer.write(json, self.path) def delete(self, url: Optional[List[str]] = None) -> None: url = url or [] diff --git a/webapp/package.json b/webapp/package.json index 7fa2c81505..df3a8f31de 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -27,6 +27,7 @@ "assert": "2.0.0", "axios": "0.27.2", "buffer": "6.0.3", + "clsx": "1.1.1", "crypto-browserify": "3.12.0", "d3": "5.16.0", "debug": "4.3.4", diff --git a/webapp/src/common/types.ts b/webapp/src/common/types.ts index fddd524ae0..ae09864a5a 100644 --- a/webapp/src/common/types.ts +++ b/webapp/src/common/types.ts @@ -594,4 +594,67 @@ export interface TaskView { status: string; } -export default {}; +export interface ThematicTrimmingConfigDTO { + "OV. COST": boolean; + "OP. COST": boolean; + "MRG. PRICE": boolean; + "CO2 EMIS.": boolean; + "DTG by plant": boolean; + BALANCE: boolean; + "ROW BAL.": boolean; + PSP: boolean; + "MISC. NDG": boolean; + LOAD: boolean; + "H. ROR": boolean; + WIND: boolean; + SOLAR: boolean; + NUCLEAR: boolean; + LIGNITE: boolean; + COAL: boolean; + GAS: boolean; + OIL: boolean; + "MIX. FUEL": boolean; + "MISC. DTG": boolean; + "H. STOR": boolean; + "H. PUMP": boolean; + "H. LEV": boolean; + "H. INFL": boolean; + "H. OVFL": boolean; + "H. VAL": boolean; + "H. COST": boolean; + "UNSP. ENRG": boolean; + "SPIL. ENRG": boolean; + LOLD: boolean; + LOLP: boolean; + "AVL DTG": boolean; + "DTG MRG": boolean; + "MAX MRG": boolean; + "NP COST": boolean; + "NP Cost by plant": boolean; + NODU: boolean; + "NODU by plant": boolean; + "FLOW LIN.": boolean; + "UCAP LIN.": boolean; + "LOOP FLOW": boolean; + "FLOW QUAD.": boolean; + "CONG. FEE (ALG.)": boolean; + "CONG. FEE (ABS.)": boolean; + "MARG. COST": boolean; + "CONG. PROD +": boolean; + "CONG. PROD -": boolean; + "HURDLE COST": boolean; + // Study version >= 810 + "RES generation by plant"?: boolean; + "MISC. DTG 2"?: boolean; + "MISC. DTG 3"?: boolean; + "MISC. DTG 4"?: boolean; + "WIND OFFSHORE"?: boolean; + "WIND ONSHORE"?: boolean; + "SOLAR CONCRT."?: boolean; + "SOLAR PV"?: boolean; + "SOLAR ROOFT"?: boolean; + "RENW. 1"?: boolean; + "RENW. 2"?: boolean; + "RENW. 3"?: boolean; + "RENW. 4"?: boolean; +} diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx index 4047c38d07..a5c09e93c3 100644 --- a/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx @@ -1,7 +1,8 @@ import * as R from "ramda"; -import { Box, Divider, TextField } from "@mui/material"; +import { Box, Button, Divider, TextField } from "@mui/material"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; +import SettingsIcon from "@mui/icons-material/Settings"; import { StyledFieldset } from "../styles"; import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; import { StudyMetadata } from "../../../../../../../common/types"; @@ -19,12 +20,13 @@ import useDebouncedEffect from "../../../../../../../hooks/useDebouncedEffect"; interface Props { study: StudyMetadata; + setDialog: React.Dispatch>; } // TODO i18n function Fields(props: Props) { - const { study } = props; + const { study, setDialog } = props; const studyVersion = Number(study.version); const [t] = useTranslation(); const { register, setValue, watch, getValues } = useFormContext(); @@ -255,14 +257,22 @@ function Fields(props: Props) { onAutoSubmit: saveValue("general/geographic-trimming"), })} /> - + + + + ) : ( (); + + //////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////// + + const getCurrentConfig = () => { + return getValues("thematicTrimmingConfig"); + }; + + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + const handleUpdateConfig = (fn: Pred) => () => { + const config = getCurrentConfig(); + const fieldNames = getFieldNames(study.version); + setValue("thematicTrimmingConfig", { + ...getCurrentConfig(), + ...R.map(fn, R.pick(fieldNames, config)), + }); + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + register("thematicTrimmingConfig", { + onAutoSubmit: () => { + const config = getCurrentConfig(); + const configDTO = thematicTrimmingConfigToDTO(config); + return setThematicTrimmingConfig(study.id, configDTO); + }, + }); + + return ( + {t("button.close")}} + contentProps={{ + sx: { pb: 0 }, + }} + > + + + + + + + + {getColumns(study.version).map((column, index) => ( + + {column.map(([label, name]) => ( + + ))} + + ))} + + + ); +} + +export default ThematicTrimmingDialog; diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/General/dialogs/ThematicTrimmingDialog/utils.ts b/webapp/src/components/App/Singlestudy/explore/Configuration/General/dialogs/ThematicTrimmingDialog/utils.ts new file mode 100644 index 0000000000..81a3833de4 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/dialogs/ThematicTrimmingDialog/utils.ts @@ -0,0 +1,196 @@ +import { camelCase } from "lodash"; +import * as R from "ramda"; +import * as RA from "ramda-adjunct"; +import { + StudyMetadata, + ThematicTrimmingConfigDTO, +} from "../../../../../../../../common/types"; + +export interface ThematicTrimmingConfig { + ovCost: boolean; + opCost: boolean; + mrgPrice: boolean; + co2Emis: boolean; + dtgByPlant: boolean; + balance: boolean; + rowBal: boolean; + psp: boolean; + miscNdg: boolean; + load: boolean; + hRor: boolean; + wind: boolean; + solar: boolean; + nuclear: boolean; + lignite: boolean; + coal: boolean; + gas: boolean; + oil: boolean; + mixFuel: boolean; + miscDtg: boolean; + hStor: boolean; + hPump: boolean; + hLev: boolean; + hInfl: boolean; + hOvfl: boolean; + hVal: boolean; + hCost: boolean; + unspEnrg: boolean; + spilEnrg: boolean; + lold: boolean; + lolp: boolean; + avlDtg: boolean; + dtgMrg: boolean; + maxMrg: boolean; + npCost: boolean; + npCostByPlant: boolean; + nodu: boolean; + noduByPlant: boolean; + flowLin: boolean; + ucapLin: boolean; + loopFlow: boolean; + flowQuad: boolean; + congFeeAlg: boolean; + congFeeAbs: boolean; + margCost: boolean; + congProdPlus: boolean; + congProdMinus: boolean; + hurdleCost: boolean; + // Study version >= 810 + resGenerationByPlant?: boolean; + miscDtg2?: boolean; + miscDtg3?: boolean; + miscDtg4?: boolean; + windOffshore?: boolean; + windOnshore?: boolean; + solarConcrt?: boolean; + solarPv?: boolean; + solarRooft?: boolean; + renw1?: boolean; + renw2?: boolean; + renw3?: boolean; + renw4?: boolean; +} + +const keysMap = { + ovCost: "OV. COST", + opCost: "OP. COST", + mrgPrice: "MRG. PRICE", + co2Emis: "CO2 EMIS.", + dtgByPlant: "DTG by plant", + balance: "BALANCE", + rowBal: "ROW BAL.", + psp: "PSP", + miscNdg: "MISC. NDG", + load: "LOAD", + hRor: "H. ROR", + wind: "WIND", + solar: "SOLAR", + nuclear: "NUCLEAR", + lignite: "LIGNITE", + coal: "COAL", + gas: "GAS", + oil: "OIL", + mixFuel: "MIX. FUEL", + miscDtg: "MISC. DTG", + hStor: "H. STOR", + hPump: "H. PUMP", + hLev: "H. LEV", + hInfl: "H. INFL", + hOvfl: "H. OVFL", + hVal: "H. VAL", + hCost: "H. COST", + unspEnrg: "UNSP. ENRG", + spilEnrg: "SPIL. ENRG", + lold: "LOLD", + lolp: "LOLP", + avlDtg: "AVL DTG", + dtgMrg: "DTG MRG", + maxMrg: "MAX MRG", + npCost: "NP COST", + npCostByPlant: "NP Cost by plant", + nodu: "NODU", + noduByPlant: "NODU by plant", + flowLin: "FLOW LIN.", + ucapLin: "UCAP LIN.", + loopFlow: "LOOP FLOW", + flowQuad: "FLOW QUAD.", + congFeeAlg: "CONG. FEE (ALG.)", + congFeeAbs: "CONG. FEE (ABS.)", + margCost: "MARG. COST", + congProdPlus: "CONG. PROD +", + congProdMinus: "CONG. PROD -", + hurdleCost: "HURDLE COST", + resGenerationByPlant: "RES generation by plant", + miscDtg2: "MISC. DTG 2", + miscDtg3: "MISC. DTG 3", + miscDtg4: "MISC. DTG 4", + windOffshore: "WIND OFFSHORE", + windOnshore: "WIND ONSHORE", + solarConcrt: "SOLAR CONCRT.", + solarPv: "SOLAR PV", + solarRooft: "SOLAR ROOFT", + renw1: "RENW. 1", + renw2: "RENW. 2", + renw3: "RENW. 3", + renw4: "RENW. 4", +}; + +export function formatThematicTrimmingConfigDTO( + configDTO: ThematicTrimmingConfigDTO +): ThematicTrimmingConfig { + return Object.entries(configDTO).reduce((acc, [key, value]) => { + const newKey = R.cond([ + [R.equals("CONG. PROD +"), R.always("congProdPlus")], + [R.equals("CONG. PROD -"), R.always("congProdMinus")], + [R.T, camelCase], + ])(key) as keyof ThematicTrimmingConfig; + + acc[newKey] = value; + return acc; + }, {} as ThematicTrimmingConfig); +} + +export function thematicTrimmingConfigToDTO( + config: ThematicTrimmingConfig +): ThematicTrimmingConfigDTO { + return RA.renameKeys(keysMap, config) as ThematicTrimmingConfigDTO; +} + +export function getColumns( + studyVersion: StudyMetadata["version"] +): Array> { + const version = Number(studyVersion); + + return [ + [ + ["OV. Cost", "ovCost"], + ["CO2 Emis.", "co2Emis"], + ["Balance", "balance"], + ["MISC. NDG", "miscNdg"], + ["Wind", "wind"], + ["Lignite", "lignite"], + ], + [ + ["OP. Cost", "opCost"], + ["DTG by plant", "dtgByPlant"], + ["Row bal.", "rowBal"], + ["Load", "load"], + ["Solar", "solar"], + ], + [ + ["MRG. Price", "mrgPrice"], + version >= 810 && ["RES generation by plant", "resGenerationByPlant"], + ["PSP", "psp"], + ["H. ROR", "hRor"], + ["Nuclear", "nuclear"], + ].filter(Boolean) as Array<[string, keyof ThematicTrimmingConfig]>, + ]; +} + +export function getFieldNames( + studyVersion: StudyMetadata["version"] +): Array { + return getColumns(studyVersion).flatMap((column) => { + return column.map(([, fieldName]) => fieldName); + }); +} diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx index 504ba05a26..34b42a94be 100644 --- a/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx @@ -1,5 +1,6 @@ import { useOutletContext } from "react-router"; import * as R from "ramda"; +import { useState } from "react"; import { StudyMetadata } from "../../../../../../common/types"; import usePromiseWithSnackbarError from "../../../../../../hooks/usePromiseWithSnackbarError"; import { getFormValues } from "./utils"; @@ -7,15 +8,29 @@ import { PromiseStatus } from "../../../../../../hooks/usePromise"; import Form from "../../../../../common/Form"; import Fields from "./Fields"; import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import ThematicTrimmingDialog from "./dialogs/ThematicTrimmingDialog"; function GeneralParameters() { const { study } = useOutletContext<{ study: StudyMetadata }>(); + const [dialog, setDialog] = useState<"thematicTrimming" | "">(""); const { data, status, error } = usePromiseWithSnackbarError( () => getFormValues(study.id), { errorMessage: "Cannot get study data", deps: [study.id] } // TODO i18n ); + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + const handleCloseDialog = () => { + setDialog(""); + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + return R.cond([ [ R.either(R.equals(PromiseStatus.Idle), R.equals(PromiseStatus.Pending)), @@ -26,7 +41,19 @@ function GeneralParameters() { R.equals(PromiseStatus.Resolved), () => (

- + + {R.cond([ + [ + R.equals("thematicTrimming"), + () => ( + + ), + ], + ])(dialog)} ), ], diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts b/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts index e99b91e072..71ad0048ee 100644 --- a/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts @@ -1,6 +1,13 @@ import * as RA from "ramda-adjunct"; import { StudyMetadata } from "../../../../../../common/types"; -import { getStudyData } from "../../../../../../services/api/study"; +import { + getStudyData, + getThematicTrimmingConfig, +} from "../../../../../../services/api/study"; +import { + ThematicTrimmingConfig, + formatThematicTrimmingConfigDTO, +} from "./dialogs/ThematicTrimmingDialog/utils"; enum Month { January = "january", @@ -115,10 +122,11 @@ export interface FormValues { mcScenario: SettingsGeneralDataOutput["storenewset"]; geographicTrimming: SettingsGeneralDataGeneral["geographic-trimming"]; thematicTrimming: SettingsGeneralDataGeneral["thematic-trimming"]; + thematicTrimmingConfig: ThematicTrimmingConfig; filtering: SettingsGeneralDataGeneral["filtering"]; } -const DEFAULT_VALUES: FormValues = { +const DEFAULT_VALUES: Omit = { mode: "Adequacy", firstDay: 1, lastDay: 1, @@ -162,6 +170,8 @@ export async function getFormValues( buildingMode = "Custom"; } + const thematicTrimmingConfigDto = await getThematicTrimmingConfig(studyId); + return { ...DEFAULT_VALUES, ...RA.renameKeys( @@ -188,5 +198,8 @@ export async function getFormValues( output ), buildingMode, + thematicTrimmingConfig: formatThematicTrimmingConfigDTO( + thematicTrimmingConfigDto + ), }; } diff --git a/webapp/src/components/common/Form/index.tsx b/webapp/src/components/common/Form/index.tsx index 098345cc12..ad905534f7 100644 --- a/webapp/src/components/common/Form/index.tsx +++ b/webapp/src/components/common/Form/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { FormEvent, useCallback, useRef } from "react"; +import { FormEvent, useCallback, useEffect, useRef } from "react"; import { FieldPath, FieldPathValue, @@ -26,6 +26,7 @@ import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; import BackdropLoading from "../loaders/BackdropLoading"; import useDebounce from "../../../hooks/useDebounce"; import { getDirtyValues, stringToPath, toAutoSubmitConfig } from "./utils"; +import useAutoUpdateRef from "../../../hooks/useAutoUpdateRef"; export interface SubmitHandlerData< TFieldValues extends FieldValues = FieldValues @@ -43,7 +44,7 @@ export interface UseFormRegisterReturnPlus< TFieldValues extends FieldValues = FieldValues, TFieldName extends FieldPath = FieldPath > extends UseFormRegisterReturn { - defaultValue?: FieldPathValue; + value?: FieldPathValue; error?: boolean; helperText?: string; } @@ -115,8 +116,7 @@ function Form( mode: "onChange", ...config, }); - const { handleSubmit, formState, register, unregister, reset, setValue } = - formObj; + const { handleSubmit, formState, reset, watch } = formObj; const { isValid, isSubmitting, isDirty, dirtyFields, errors } = formState; const allowSubmit = isDirty && isValid && !isSubmitting; const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); @@ -127,6 +127,16 @@ function Form( Record any | Promise) | undefined> >({}); const lastDataSubmitted = useRef>(); + const preventClose = useRef(false); + const watchAllFields = watch(); + const wrapperFnsData = useAutoUpdateRef({ + fieldValues: watchAllFields, + fieldErrors: errors, + register: formObj.register, + unregister: formObj.unregister, + setValue: formObj.setValue, + isAutoConfigEnabled: autoSubmitConfig.enable, + }); useUpdateEffect( () => { @@ -141,6 +151,21 @@ function Form( [formState] ); + useEffect(() => { + const listener = (event: BeforeUnloadEvent) => { + if (preventClose.current) { + // eslint-disable-next-line no-param-reassign + event.returnValue = "Form not submitted yet. Sure you want to leave?"; // TODO i18n + } + }; + + window.addEventListener("beforeunload", listener); + + return () => { + window.removeEventListener("beforeunload", listener); + }; + }, []); + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// @@ -174,21 +199,37 @@ function Form( } return Promise.all(res); - })().catch((error) => { - enqueueErrorSnackbar(t("form.submit.error"), error); - }); + })() + .catch((error) => { + enqueueErrorSnackbar(t("form.submit.error"), error); + }) + .finally(() => { + preventClose.current = false; + }); }; //////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////// - const simulateSubmit = useDebounce(() => { + const simulateSubmitClick = useDebounce(() => { submitRef.current?.click(); }, autoSubmitConfig.wait); + const simulateSubmit = useAutoUpdateRef(() => { + preventClose.current = true; + simulateSubmitClick(); + }); + + //////////////////////////////////////////////////////////////// + // API + //////////////////////////////////////////////////////////////// + const registerWrapper = useCallback>( (name, options) => { + const { register, fieldValues, fieldErrors, isAutoConfigEnabled } = + wrapperFnsData.current; + if (options?.onAutoSubmit) { fieldAutoSubmitListeners.current[name] = options.onAutoSubmit; } @@ -197,8 +238,8 @@ function Form( ...options, onChange: (e: unknown) => { options?.onChange?.(e); - if (autoSubmitConfig.enable) { - simulateSubmit(); + if (isAutoConfigEnabled) { + simulateSubmit.current(); } }, }; @@ -208,11 +249,9 @@ function Form( typeof name >; - const error = errors[name]; + const error = fieldErrors[name]; - if (RA.isNotNil(config?.defaultValues?.[name])) { - res.defaultValue = config?.defaultValues?.[name]; - } + res.value = R.path(name.split("."), fieldValues); if (error) { res.error = true; @@ -223,17 +262,14 @@ function Form( return res; }, - [ - autoSubmitConfig.enable, - config?.defaultValues, - errors, - register, - simulateSubmit, - ] + // eslint-disable-next-line react-hooks/exhaustive-deps + [] ); const unregisterWrapper = useCallback>( (name, options) => { + const { unregister } = wrapperFnsData.current; + if (name) { const names = RA.ensureArray(name) as Path[]; names.forEach((n) => { @@ -242,23 +278,27 @@ function Form( } return unregister(name, options); }, - [unregister] + // eslint-disable-next-line react-hooks/exhaustive-deps + [] ); const setValueWrapper = useCallback>( (name, value, options) => { + const { setValue, isAutoConfigEnabled } = wrapperFnsData.current; + const newOptions: typeof options = { - shouldDirty: autoSubmitConfig.enable, // Option false by default + shouldDirty: isAutoConfigEnabled, // Option false by default ...options, }; - if (autoSubmitConfig.enable && newOptions.shouldDirty) { - simulateSubmit(); + if (isAutoConfigEnabled && newOptions.shouldDirty) { + simulateSubmit.current(); } setValue(name, value, newOptions); }, - [autoSubmitConfig.enable, setValue, simulateSubmit] + // eslint-disable-next-line react-hooks/exhaustive-deps + [] ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/common/fieldEditors/SelectFE.tsx b/webapp/src/components/common/fieldEditors/SelectFE.tsx index 2728a72b70..d72d586e3b 100644 --- a/webapp/src/components/common/fieldEditors/SelectFE.tsx +++ b/webapp/src/components/common/fieldEditors/SelectFE.tsx @@ -56,12 +56,7 @@ const SelectFE = forwardRef((props: SelectFEProps, ref) => { return ( {label} - {emptyValue && ( {/* TODO i18n */} diff --git a/webapp/src/components/common/fieldEditors/SwitchFE.tsx b/webapp/src/components/common/fieldEditors/SwitchFE.tsx index d7c5b3e33b..a990903916 100644 --- a/webapp/src/components/common/fieldEditors/SwitchFE.tsx +++ b/webapp/src/components/common/fieldEditors/SwitchFE.tsx @@ -4,6 +4,7 @@ import { Switch, SwitchProps, } from "@mui/material"; +import clsx from "clsx"; import { forwardRef } from "react"; export interface SwitchFEProps @@ -27,6 +28,7 @@ const SwitchFE = forwardRef((props: SwitchFEProps, ref) => { labelPlacement, helperText, error, + className, sx, ...rest } = props; @@ -34,6 +36,7 @@ const SwitchFE = forwardRef((props: SwitchFEProps, ref) => { const fieldEditor = ( { return ( (value: T): React.MutableRefObject { + const ref = useRef(value); + + useEffect(() => { + ref.current = value; + }); + + return ref; +} + +export default useAutoUpdateRef; diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 03ec6563e9..3ba47cc781 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -13,6 +13,7 @@ import { AreasConfig, LaunchJobDTO, StudyMetadataPatchDTO, + ThematicTrimmingConfigDTO, } from "../../common/types"; import { getConfig } from "../config"; import { convertStudyDtoToMetadata } from "../utils"; @@ -389,4 +390,18 @@ export const scanFolder = async (folderPath: string): Promise => { await client.post(`/v1/watcher/_scan?path=${encodeURIComponent(folderPath)}`); }; -export default {}; +export const getThematicTrimmingConfig = async ( + studyId: StudyMetadata["id"] +): Promise => { + const res = await client.get( + `/v1/studies/${studyId}/config/thematic_trimming` + ); + return res.data; +}; + +export const setThematicTrimmingConfig = async ( + studyId: StudyMetadata["id"], + config: ThematicTrimmingConfigDTO +): Promise => { + await client.put(`/v1/studies/${studyId}/config/thematic_trimming`, config); +}; diff --git a/webapp/src/theme.ts b/webapp/src/theme.ts index 0cb4c1dce9..bfb2d709b5 100644 --- a/webapp/src/theme.ts +++ b/webapp/src/theme.ts @@ -11,7 +11,6 @@ export const STUDIES_FILTER_WIDTH = 300; const secondaryMainColor = "#00B2FF"; export const PAPER_BACKGROUND_NO_TRANSPARENCY = "#212c38"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const scrollbarStyle = { "&::-webkit-scrollbar": { width: "7px", @@ -130,7 +129,6 @@ const theme = createTheme({ background: "rgba(255, 255, 255, 0.09)", borderRadius: "4px 4px 0px 0px", borderBottom: "1px solid rgba(255, 255, 255, 0.42)", - paddingRight: 6, ".MuiSelect-icon": { backgroundColor: "#222333", }, From 3ef9af6cb0e0a2571b8d657f98b04557d9bc6d63 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Mon, 4 Jul 2022 16:25:23 +0200 Subject: [PATCH 17/24] Set default to zipped ouput import (#977) --- antarest/launcher/model.py | 2 +- antarest/launcher/service.py | 5 ++--- tests/launcher/test_service.py | 12 +++++++++--- webapp/src/components/App/Studies/LauncherDialog.tsx | 11 ----------- webapp/src/services/api/study.ts | 2 -- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/antarest/launcher/model.py b/antarest/launcher/model.py index 9fe16e3395..5ec663243f 100644 --- a/antarest/launcher/model.py +++ b/antarest/launcher/model.py @@ -17,7 +17,7 @@ class LauncherParametersDTO(BaseModel): time_limit: Optional[int] = None xpansion: bool = False xpansion_r_version: bool = False - archive_output: bool = False + archive_output: bool = True output_suffix: Optional[str] = None other_options: Optional[str] = None # add extensions field here diff --git a/antarest/launcher/service.py b/antarest/launcher/service.py index 302420aac9..21e3412713 100644 --- a/antarest/launcher/service.py +++ b/antarest/launcher/service.py @@ -500,15 +500,14 @@ def _import_fallback_output( job_output_path = self._get_job_output_fallback_path(job_id) try: + output_name = extract_output_name(output_path, output_suffix_name) os.mkdir(job_output_path) if output_path.suffix != ".zip": imported_output_path = job_output_path / "imported" shutil.copytree(output_path, imported_output_path) - output_name = extract_output_name( - imported_output_path, output_suffix_name - ) imported_output_path.rename(Path(job_output_path, output_name)) else: + shutil.copy( output_path, job_output_path / f"{output_name}.zip" ) diff --git a/tests/launcher/test_service.py b/tests/launcher/test_service.py index 88380a9413..deaa4c1109 100644 --- a/tests/launcher/test_service.py +++ b/tests/launcher/test_service.py @@ -478,7 +478,7 @@ def test_get_logs(tmp_path: Path): JobLog(message="second message", log_type=str(JobLogType.BEFORE)), JobLog(message="last message", log_type=str(JobLogType.AFTER)), ] - job_result_mock.launcher_params = None + job_result_mock.launcher_params = '{"archive_output": false}' launcher_service.job_result_repository.get.return_value = job_result_mock slurm_launcher = Mock() @@ -569,12 +569,18 @@ def test_manage_output(tmp_path: Path): None, JobResult(id=job_id, study_id=study_id), JobResult(id=job_id, study_id=study_id, output_id="some id"), - JobResult(id=job_id, study_id=study_id), + JobResult( + id=job_id, + study_id=study_id, + ), JobResult( id=job_id, study_id=study_id, launcher_params=json.dumps( - {f"{LAUNCHER_PARAM_NAME_SUFFIX}": "hello"} + { + "archive_output": False, + f"{LAUNCHER_PARAM_NAME_SUFFIX}": "hello", + } ), ), ] diff --git a/webapp/src/components/App/Studies/LauncherDialog.tsx b/webapp/src/components/App/Studies/LauncherDialog.tsx index e4578749ea..125ab569d3 100644 --- a/webapp/src/components/App/Studies/LauncherDialog.tsx +++ b/webapp/src/components/App/Studies/LauncherDialog.tsx @@ -256,17 +256,6 @@ function LauncherDialog(props: Props) { width: "100%", }} > - { - handleChange("archive_output", checked); - }} - /> - } - label={t("study.archiveOutputMode") as string} - /> Date: Tue, 5 Jul 2022 11:35:13 +0200 Subject: [PATCH 18/24] Fix folder filter Signed-off-by: Paul Bui-Quang --- webapp/src/utils/studiesUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webapp/src/utils/studiesUtils.ts b/webapp/src/utils/studiesUtils.ts index 4096d0c71c..31c0894690 100644 --- a/webapp/src/utils/studiesUtils.ts +++ b/webapp/src/utils/studiesUtils.ts @@ -41,7 +41,9 @@ const folderPredicate = R.curry( studyNodeId += `/${folderPath}`; } } - return strict ? studyNodeId === folder : studyNodeId.startsWith(folder); + return strict + ? studyNodeId === folder + : `${studyNodeId}/`.startsWith(`${folder}/`); } ); From 857750670d71286878da37b54e89326921543b6a Mon Sep 17 00:00:00 2001 From: Wintxer <47366828+Wintxer@users.noreply.github.com> Date: Tue, 5 Jul 2022 12:38:35 +0200 Subject: [PATCH 19/24] Link styles in map (#970) --- antarest/study/business/link_management.py | 6 +-- webapp/src/common/types.ts | 12 +++++- .../explore/Modelization/Map/index.tsx | 26 +++++++------ .../explore/Modelization/Map/utils.ts | 38 +++++++++++++++++++ .../Xpansion/Candidates/CandidateForm.tsx | 4 +- .../Candidates/CreateCandidateDialog.tsx | 4 +- .../explore/Xpansion/Candidates/index.tsx | 2 +- .../App/Studies/StudiesList/index.tsx | 2 +- webapp/src/services/api/studydata.ts | 25 +++++++++--- 9 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 webapp/src/components/App/Singlestudy/explore/Modelization/Map/utils.ts diff --git a/antarest/study/business/link_management.py b/antarest/study/business/link_management.py index a4d20db1a8..4556bfd667 100644 --- a/antarest/study/business/link_management.py +++ b/antarest/study/business/link_management.py @@ -44,9 +44,9 @@ def get_all_links( ui_info: Optional[LinkUIDTO] = None if with_ui and links_config and link in links_config: ui_info = LinkUIDTO( - color=f"{links_config[link]['colorr']},{links_config[link]['colorg']},{links_config[link]['colorb']}", - width=links_config[link]["link-width"], - style=links_config[link]["link-style"], + color=f"{links_config[link].get('colorr', '163')},{links_config[link].get('colorg', '163')},{links_config[link].get('colorb', '163')}", + width=links_config[link].get("link-width", 1), + style=links_config[link].get("link-style", "plain"), ) result.append( LinkInfoDTO(area1=area_id, area2=link, ui=ui_info) diff --git a/webapp/src/common/types.ts b/webapp/src/common/types.ts index ae09864a5a..b506ad173c 100644 --- a/webapp/src/common/types.ts +++ b/webapp/src/common/types.ts @@ -563,11 +563,21 @@ export interface UpdateAreaUi { color_rgb: Array; } -export interface LinkCreationInfo { +export interface LinkUIInfoDTO { + color: string; + style: string; + width: number; +} + +export interface LinkCreationInfoDTO { area1: string; area2: string; } +export interface LinkInfoWithUI extends LinkCreationInfoDTO { + ui: LinkUIInfoDTO; +} + export interface AreaCreationDTO { name: string; type: object; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx index c4532cd5eb..2cac0ab02f 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx @@ -20,6 +20,7 @@ import { deleteArea, deleteLink, createLink, + getAllLinks, } from "../../../../../../services/api/studydata"; import { getAreaPositions, @@ -33,6 +34,7 @@ import mapbackground from "./mapbackground.png"; import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; import { setCurrentArea } from "../../../../../../redux/ducks/studyDataSynthesis"; import useAppDispatch from "../../../../../../redux/hooks/useAppDispatch"; +import { linkStyle } from "./utils"; const FONT_SIZE = 16; const NODE_HEIGHT = 400; @@ -300,19 +302,19 @@ function Map() { }; }); setNodeData(tempNodeData); + const links = await getAllLinks({ uuid: study.id, withUi: true }); setLinkData( - Object.keys(data.areas).reduce( - (links, currentAreaId) => - links.concat( - Object.keys(data.areas[currentAreaId].links).map( - (linkId) => ({ - source: currentAreaId, - target: linkId, - }) - ) - ), - [] as Array - ) + links.map((link) => { + const [style, linecap] = linkStyle(link.ui?.style); + return { + source: link.area1, + target: link.area2, + color: `rgb(${link.ui?.color}`, + strokeDasharray: style, + strokeLinecap: linecap, + strokeWidth: link.ui?.width, + }; + }) ); } } catch (e) { diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Map/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/utils.ts new file mode 100644 index 0000000000..58cc038a00 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/utils.ts @@ -0,0 +1,38 @@ +import * as R from "ramda"; + +export const linkStyle = (linkStyle: string): Array | string> => { + const linkCond = R.cond([ + [ + R.equals("dot"), + (): Array | string> => { + return [[1, 5], "round"]; + }, + ], + [ + R.equals("dash"), + (): Array | string> => { + return [[16, 8], "square"]; + }, + ], + [ + R.equals("dotdash"), + (): Array | string> => { + return [[10, 6, 1, 6], "square"]; + }, + ], + [ + (_: string): boolean => true, + (): Array | string> => { + return [[0], "butt"]; + }, + ], + ]); + + const values = linkCond(linkStyle); + const style = values[0] as Array; + const linecap = values[1] as string; + + return [style, linecap]; +}; + +export default {}; diff --git a/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx index a0b86080ab..578e969158 100644 --- a/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx @@ -21,13 +21,13 @@ import { StyledVisibilityIcon, StyledDeleteIcon, } from "../share/styles"; -import { LinkCreationInfo } from "../../../../../../common/types"; +import { LinkCreationInfoDTO } from "../../../../../../common/types"; import { XpansionCandidate } from "../types"; import SelectSingle from "../../../../../common/SelectSingle"; interface PropType { candidate: XpansionCandidate | undefined; - links: Array; + links: Array; capacities: Array; deleteCandidate: (name: string | undefined) => Promise; updateCandidate: ( diff --git a/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx index f2357deadd..a64c033498 100644 --- a/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { TextField, Button, Box, Divider, ButtonGroup } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { LinkCreationInfo } from "../../../../../../common/types"; +import { LinkCreationInfoDTO } from "../../../../../../common/types"; import { XpansionCandidate } from "../types"; import SelectSingle from "../../../../../common/SelectSingle"; import { HoverButton, ActiveButton } from "../share/styles"; @@ -9,7 +9,7 @@ import BasicDialog from "../../../../../common/dialogs/BasicDialog"; interface PropType { open: boolean; - links: Array; + links: Array; onClose: () => void; onSave: (candidate: XpansionCandidate) => void; } diff --git a/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx index 700f7b0e1d..104f89858b 100644 --- a/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx @@ -89,7 +89,7 @@ function Candidates() { if (exist) { return { capacities: await getAllCapacities(study.id), - links: await getAllLinks(study.id), + links: await getAllLinks({ uuid: study.id }), }; } return {}; diff --git a/webapp/src/components/App/Studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx index 34c9f7e98a..cb329ab4cf 100644 --- a/webapp/src/components/App/Studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -222,7 +222,7 @@ function StudiesList(props: StudiesListProps) { ({`${studyIds.length} ${t("global.studies").toLowerCase()}`}) - + => { const res = await client.post( `/v1/studies/${uuid}/links?uuid=${uuid}`, @@ -60,10 +61,22 @@ export const deleteLink = async ( return res.data; }; -export const getAllLinks = async ( - uuid: string -): Promise> => { - const res = await client.get(`/v1/studies/${uuid}/links`); +interface GetAllLinksParams { + uuid: string; + withUi?: boolean; +} + +type LinkTypeFromParams = T["withUi"] extends true + ? LinkInfoWithUI + : LinkCreationInfoDTO; + +export const getAllLinks = async ( + params: T +): Promise>> => { + const { uuid, withUi } = params; + const res = await client.get( + `/v1/studies/${uuid}/links${withUi ? `?with_ui=${withUi}` : ""}` + ); return res.data; }; From fb475a908ec1cb7162a17d50abdabb6a8c4caa68 Mon Sep 17 00:00:00 2001 From: Paul Bui-Quang Date: Wed, 6 Jul 2022 09:52:45 +0200 Subject: [PATCH 20/24] Add task workers (#915) --- antarest/core/config.py | 7 +- antarest/core/interfaces/eventbus.py | 41 +++++- antarest/core/tasks/model.py | 1 + antarest/core/tasks/service.py | 120 ++++++++++++++++-- antarest/core/utils/utils.py | 18 ++- antarest/eventbus/business/interfaces.py | 19 ++- antarest/eventbus/business/local_eventbus.py | 13 +- antarest/eventbus/business/redis_eventbus.py | 11 +- antarest/eventbus/service.py | 88 +++++++++++-- .../adapters/slurm_launcher/slurm_launcher.py | 3 +- antarest/main.py | 24 +++- antarest/worker/__init__.py | 0 antarest/worker/archive_worker.py | 10 ++ antarest/worker/worker.py | 70 ++++++++++ tests/conftest.py | 13 +- tests/core/test_tasks.py | 84 +++++++++++- tests/core/test_utils.py | 11 +- tests/eventbus/test_local_eventbus.py | 4 +- tests/eventbus/test_redis_event_bus.py | 4 +- tests/eventbus/test_service.py | 54 +++++--- tests/storage/conftest.py | 12 +- tests/worker/__init__.py | 0 tests/worker/test_worker.py | 46 +++++++ 23 files changed, 576 insertions(+), 77 deletions(-) create mode 100644 antarest/worker/__init__.py create mode 100644 antarest/worker/archive_worker.py create mode 100644 antarest/worker/worker.py create mode 100644 tests/worker/__init__.py create mode 100644 tests/worker/test_worker.py diff --git a/antarest/core/config.py b/antarest/core/config.py index 2e3107b99e..49e29a0457 100644 --- a/antarest/core/config.py +++ b/antarest/core/config.py @@ -247,10 +247,15 @@ class RedisConfig: host: str = "localhost" port: int = 6379 + password: Optional[str] = None @staticmethod def from_dict(data: JSON) -> "RedisConfig": - return RedisConfig(host=data["host"], port=data["port"]) + return RedisConfig( + host=data["host"], + port=data["port"], + password=data.get("password", None), + ) @dataclass(frozen=True) diff --git a/antarest/core/interfaces/eventbus.py b/antarest/core/interfaces/eventbus.py index 0c6af3063f..4c751ae234 100644 --- a/antarest/core/interfaces/eventbus.py +++ b/antarest/core/interfaces/eventbus.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from enum import Enum from typing import Any, Callable, Optional, List, Awaitable from pydantic import BaseModel @@ -6,7 +7,8 @@ from antarest.core.model import PermissionInfo -class EventType: +class EventType(str, Enum): + ANY = "_ANY" STUDY_CREATED = "STUDY_CREATED" STUDY_DELETED = "STUDY_DELETED" STUDY_EDITED = "STUDY_EDITED" @@ -31,6 +33,9 @@ class EventType: DOWNLOAD_FAILED = "DOWNLOAD_FAILED" MESSAGE_INFO = "MESSAGE_INFO" MAINTENANCE_MODE = "MAINTENANCE_MODE" + WORKER_TASK = "WORKER_TASK" + WORKER_TASK_STARTED = "WORKER_TASK_STARTED" + WORKER_TASK_ENDED = "WORKER_TASK_ENDED" class EventChannelDirectory: @@ -41,7 +46,7 @@ class EventChannelDirectory: class Event(BaseModel): - type: str + type: EventType payload: Any permissions: PermissionInfo = PermissionInfo() channel: Optional[str] = None @@ -52,11 +57,25 @@ class IEventBus(ABC): def push(self, event: Event) -> None: pass + @abstractmethod + def queue(self, event: Event, queue: str) -> None: + pass + + @abstractmethod + def add_queue_consumer( + self, listener: Callable[[Event], Awaitable[None]], queue: str + ) -> str: + pass + + @abstractmethod + def remove_queue_consumer(self, listener_id: str) -> None: + pass + @abstractmethod def add_listener( self, listener: Callable[[Event], Awaitable[None]], - type_filter: Optional[List[str]] = None, + type_filter: Optional[List[EventType]] = None, ) -> str: """ Add an event listener listener @@ -77,6 +96,20 @@ def start(self, threaded: bool = True) -> None: class DummyEventBusService(IEventBus): + def queue(self, event: Event, queue: str) -> None: + # Noop + pass + + def add_queue_consumer( + self, listener: Callable[[Event], Awaitable[None]], queue: str + ) -> str: + # Noop + pass + + def remove_queue_consumer(self, listener_id: str) -> None: + # Noop + pass + def push(self, event: Event) -> None: # Noop pass @@ -84,7 +117,7 @@ def push(self, event: Event) -> None: def add_listener( self, listener: Callable[[Event], Awaitable[None]], - type_filter: Optional[List[str]] = None, + type_filter: Optional[List[EventType]] = None, ) -> str: return "" diff --git a/antarest/core/tasks/model.py b/antarest/core/tasks/model.py index 7af6b49295..e8e7cf2e0b 100644 --- a/antarest/core/tasks/model.py +++ b/antarest/core/tasks/model.py @@ -17,6 +17,7 @@ class TaskType(str, Enum): ARCHIVE = "ARCHIVE" UNARCHIVE = "UNARCHIVE" SCAN = "SCAN" + WORKER_TASK = "WORKER_TASK" class TaskStatus(Enum): diff --git a/antarest/core/tasks/service.py b/antarest/core/tasks/service.py index 23cc552520..a3697011ce 100644 --- a/antarest/core/tasks/service.py +++ b/antarest/core/tasks/service.py @@ -6,7 +6,7 @@ from concurrent.futures import ThreadPoolExecutor, Future from enum import Enum from http import HTTPStatus -from typing import Callable, Optional, List, Dict, Awaitable +from typing import Callable, Optional, List, Dict, Awaitable, Union, cast from fastapi import HTTPException @@ -38,6 +38,7 @@ from antarest.core.tasks.repository import TaskJobRepository from antarest.core.utils.fastapi_sqlalchemy import db from antarest.core.utils.utils import retry +from antarest.worker.worker import WorkerTaskCommand, WorkerTaskResult logger = logging.getLogger(__name__) @@ -46,6 +47,17 @@ class ITaskService(ABC): + @abstractmethod + def add_worker_task( + self, + task_type: str, + task_args: Dict[str, Union[int, float, bool, str]], + name: Optional[str], + ref_id: Optional[str], + request_params: RequestParameters, + ) -> str: + raise NotImplementedError() + @abstractmethod def add_task( self, @@ -101,10 +113,72 @@ def __init__( self.threadpool = ThreadPoolExecutor( max_workers=config.tasks.max_workers, thread_name_prefix="taskjob_" ) - self.event_bus.add_listener(self.create_task_event_callback()) + self.event_bus.add_listener( + self.create_task_event_callback(), [EventType.TASK_CANCEL_REQUEST] + ) # set the status of previously running job to FAILED due to server restart self._fix_running_status() + def _create_worker_task( + self, + task_id: str, + task_type: str, + task_args: Dict[str, Union[int, float, bool, str]], + ) -> Callable[[TaskUpdateNotifier], TaskResult]: + task_result_wrapper: List[TaskResult] = [] + + def _create_awaiter( + res_wrapper: List[TaskResult], + ) -> Callable[[Event], Awaitable[None]]: + async def _await_task_end(event: Event) -> None: + task_event = cast(WorkerTaskResult, event.payload) + if task_event.task_id == task_id: + res_wrapper.append(task_event.task_result) + + return _await_task_end + + def _send_worker_task(logger: TaskUpdateNotifier) -> TaskResult: + listener_id = self.event_bus.add_listener( + _create_awaiter(task_result_wrapper), + [EventType.WORKER_TASK_ENDED], + ) + self.event_bus.queue( + Event( + type=EventType.WORKER_TASK, + payload=WorkerTaskCommand( + task_id=task_id, + task_type=task_type, + task_args=task_args, + ), + ), + task_type, + ) + while not task_result_wrapper: + time.sleep(1) + self.event_bus.remove_listener(listener_id) + return task_result_wrapper[0] + + return _send_worker_task + + def add_worker_task( + self, + task_type: str, + task_args: Dict[str, Union[int, float, bool, str]], + name: Optional[str], + ref_id: Optional[str], + request_params: RequestParameters, + ) -> str: + task = self._create_task( + name, TaskType.WORKER_TASK, ref_id, request_params + ) + self._launch_task( + self._create_worker_task(str(task.id), task_type, task_args), + task, + None, + request_params, + ) + return str(task.id) + def add_task( self, action: Task, @@ -114,10 +188,21 @@ def add_task( custom_event_messages: Optional[CustomTaskEventMessages], request_params: RequestParameters, ) -> str: + task = self._create_task(name, task_type, ref_id, request_params) + self._launch_task(action, task, custom_event_messages, request_params) + return str(task.id) + + def _create_task( + self, + name: Optional[str], + task_type: Optional[TaskType], + ref_id: Optional[str], + request_params: RequestParameters, + ) -> TaskJob: if not request_params.user: raise MustBeAuthenticatedError() - task = self.repo.save( + return self.repo.save( TaskJob( name=name or "Unnamed", owner_id=request_params.user.impersonator, @@ -126,6 +211,16 @@ def add_task( ) ) + def _launch_task( + self, + action: Task, + task: TaskJob, + custom_event_messages: Optional[CustomTaskEventMessages], + request_params: RequestParameters, + ) -> None: + if not request_params.user: + raise MustBeAuthenticatedError() + self.event_bus.push( Event( type=EventType.TASK_ADDED, @@ -144,12 +239,10 @@ def add_task( self._run_task, action, task.id, custom_event_messages ) self.tasks[task.id] = future - return str(task.id) def create_task_event_callback(self) -> Callable[[Event], Awaitable[None]]: async def task_event_callback(event: Event) -> None: - if event.type == EventType.TASK_CANCEL_REQUEST: - self._cancel_task(str(event.payload), dispatch=False) + self._cancel_task(str(event.payload), dispatch=False) return task_event_callback @@ -227,13 +320,14 @@ def await_task( ) end = time.time() + (timeout_sec or DEFAULT_AWAIT_MAX_TIMEOUT) while time.time() < end: - task = self.repo.get(task_id) - if not task: - logger.error(f"Awaited task {task_id} was not found") - break - if TaskStatus(task.status).is_final(): - break - time.sleep(2) + with db(): + task = self.repo.get(task_id) + if not task: + logger.error(f"Awaited task {task_id} was not found") + break + if TaskStatus(task.status).is_final(): + break + time.sleep(2) def _run_task( self, diff --git a/antarest/core/utils/utils.py b/antarest/core/utils/utils.py index e4a876569c..c00c087989 100644 --- a/antarest/core/utils/utils.py +++ b/antarest/core/utils/utils.py @@ -3,7 +3,7 @@ import time from glob import escape from pathlib import Path -from typing import IO, Any, Optional, Callable, TypeVar, List +from typing import IO, Any, Optional, Callable, TypeVar, List, Union, Awaitable from zipfile import ( ZipFile, BadZipFile, @@ -90,7 +90,10 @@ def get_local_path() -> Path: def new_redis_instance(config: RedisConfig) -> redis.Redis: # type: ignore - return redis.Redis(host=config.host, port=config.port, db=0) + redis_client = redis.Redis( + host=config.host, port=config.port, password=config.password, db=0 + ) + return redis_client class StopWatch: @@ -164,3 +167,14 @@ def unzip( zipf.extractall(dir_path) if remove_source_zip: zip_path.unlink() + + +def suppress_exception( + callback: Callable[[], T], + logger: Callable[[Exception], None], +) -> Optional[T]: + try: + return callback() + except Exception as e: + logger(e) + return None diff --git a/antarest/eventbus/business/interfaces.py b/antarest/eventbus/business/interfaces.py index 6eb614b149..b180775c4f 100644 --- a/antarest/eventbus/business/interfaces.py +++ b/antarest/eventbus/business/interfaces.py @@ -1,18 +1,27 @@ +import abc from abc import abstractmethod -from typing import List +from typing import List, Optional from antarest.core.interfaces.eventbus import Event -class IEventBusBackend: +class IEventBusBackend(abc.ABC): @abstractmethod def push_event(self, event: Event) -> None: - pass + raise NotImplementedError + + @abstractmethod + def queue_event(self, event: Event, queue: str) -> None: + raise NotImplementedError + + @abstractmethod + def pull_queue(self, queue: str) -> Optional[Event]: + raise NotImplementedError @abstractmethod def get_events(self) -> List[Event]: - pass + raise NotImplementedError @abstractmethod def clear_events(self) -> None: - pass + raise NotImplementedError diff --git a/antarest/eventbus/business/local_eventbus.py b/antarest/eventbus/business/local_eventbus.py index e01af50d5e..4851a1ed51 100644 --- a/antarest/eventbus/business/local_eventbus.py +++ b/antarest/eventbus/business/local_eventbus.py @@ -1,5 +1,5 @@ import logging -from typing import List +from typing import List, Dict, Optional from antarest.core.interfaces.eventbus import Event from antarest.eventbus.business.interfaces import IEventBusBackend @@ -10,6 +10,7 @@ class LocalEventBus(IEventBusBackend): def __init__(self) -> None: self.events: List[Event] = [] + self.queues: Dict[str, List[Event]] = {} def push_event(self, event: Event) -> None: self.events.append(event) @@ -19,3 +20,13 @@ def get_events(self) -> List[Event]: def clear_events(self) -> None: self.events.clear() + + def queue_event(self, event: Event, queue: str) -> None: + if queue not in self.queues: + self.queues[queue] = [] + self.queues[queue].append(event) + + def pull_queue(self, queue: str) -> Optional[Event]: + if queue in self.queues and len(self.queues[queue]) > 0: + return self.queues[queue].pop(0) + return None diff --git a/antarest/eventbus/business/redis_eventbus.py b/antarest/eventbus/business/redis_eventbus.py index bdbfc8b317..4e27a7793c 100644 --- a/antarest/eventbus/business/redis_eventbus.py +++ b/antarest/eventbus/business/redis_eventbus.py @@ -1,7 +1,7 @@ import dataclasses import json import logging -from typing import List +from typing import List, Optional, cast from redis.client import Redis @@ -22,6 +22,15 @@ def __init__(self, redis_client: Redis) -> None: # type: ignore def push_event(self, event: Event) -> None: self.redis.publish(REDIS_STORE_KEY, event.json()) + def queue_event(self, event: Event, queue: str) -> None: + self.redis.rpush(queue, event.json()) + + def pull_queue(self, queue: str) -> Optional[Event]: + event = self.redis.lpop(queue) + if event: + return cast(Optional[Event], Event.parse_raw(event)) + return None + def get_events(self) -> List[Event]: try: event = self.pubsub.get_message(ignore_subscribe_messages=True) diff --git a/antarest/eventbus/service.py b/antarest/eventbus/service.py index b2a84af6dd..a7b8cfd2c7 100644 --- a/antarest/eventbus/service.py +++ b/antarest/eventbus/service.py @@ -1,11 +1,13 @@ import asyncio import logging +import random import threading import time -from typing import List, Callable, Optional, Dict, Awaitable +from typing import List, Callable, Optional, Dict, Awaitable, Any, cast from uuid import uuid4 from antarest.core.interfaces.eventbus import Event, IEventBus, EventType +from antarest.core.utils.utils import suppress_exception from antarest.eventbus.business.interfaces import IEventBusBackend logger = logging.getLogger(__name__) @@ -16,7 +18,13 @@ def __init__( self, backend: IEventBusBackend, autostart: bool = True ) -> None: self.backend = backend - self.listeners: Dict[str, Callable[[Event], Awaitable[None]]] = {} + self.listeners: Dict[ + EventType, Dict[str, Callable[[Event], Awaitable[None]]] + ] = {ev_type: {} for ev_type in EventType} + self.consumers: Dict[ + str, Dict[str, Callable[[Event], Awaitable[None]]] + ] = {} + self.lock = threading.Lock() if autostart: self.start() @@ -24,35 +32,89 @@ def __init__( def push(self, event: Event) -> None: self.backend.push_event(event) + def queue(self, event: Event, queue: str) -> None: + self.backend.queue_event(event, queue) + + def add_queue_consumer( + self, listener: Callable[[Event], Awaitable[None]], queue: str + ) -> str: + with self.lock: + listener_id = str(uuid4()) + if queue not in self.consumers: + self.consumers[queue] = {} + self.consumers[queue][listener_id] = listener + return listener_id + + def remove_queue_consumer(self, listener_id: str) -> None: + with self.lock: + for queue in self.consumers: + if listener_id in self.consumers[queue]: + del self.consumers[queue][listener_id] + def add_listener( self, listener: Callable[[Event], Awaitable[None]], - type_filter: Optional[List[str]] = None, + type_filter: Optional[List[EventType]] = None, ) -> str: with self.lock: listener_id = str(uuid4()) - self.listeners[listener_id] = listener + types = type_filter or [EventType.ANY] + for listener_type in types: + self.listeners[listener_type][listener_id] = listener return listener_id def remove_listener(self, listener_id: str) -> None: with self.lock: - del self.listeners[listener_id] + for listener_type in self.listeners: + if listener_id in self.listeners[listener_type]: + del self.listeners[listener_type][listener_id] async def _run_loop(self) -> None: while True: time.sleep(0.2) - await self._on_events() + try: + await self._on_events() + except Exception as e: + logger.error( + f"Unexpected error when processing events", exc_info=e + ) async def _on_events(self) -> None: with self.lock: + for queue in self.consumers: + if len(self.consumers[queue]) > 0: + event = self.backend.pull_queue(queue) + while event is not None: + try: + await list(self.consumers[queue].values())[ + random.randint( + 0, len(self.consumers[queue]) - 1 + ) + ](event) + except Exception as ex: + logger.error( + f"Failed to process queue event {event.type}", + exc_info=ex, + ) + event = self.backend.pull_queue(queue) + for e in self.backend.get_events(): - for listener in self.listeners.values(): - try: - await listener(e) - except Exception as ex: - logger.error( - f"Failed to process event {e.type}", exc_info=ex - ) + if e.type in self.listeners: + responses = await asyncio.gather( + *[ + listener(e) + for listener in list( + self.listeners[e.type].values() + ) + + list(self.listeners[EventType.ANY].values()) + ] + ) + for res in responses: + if isinstance(res, Exception): + logger.error( + f"Failed to process event {e.type}", + exc_info=res, + ) self.backend.clear_events() def _async_loop(self, new_loop: bool = True) -> None: diff --git a/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py b/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py index 71c3b1e513..4ecbb86af3 100644 --- a/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py +++ b/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py @@ -568,8 +568,7 @@ def get_log(self, job_id: str, log_type: LogType) -> Optional[str]: def _create_event_listener(self) -> Callable[[Event], Awaitable[None]]: async def _listen_to_kill_job(event: Event) -> None: - if event.type == EventType.STUDY_JOB_CANCEL_REQUEST: - self.kill_job(event.payload, dispatch=False) + self.kill_job(event.payload, dispatch=False) return _listen_to_kill_job diff --git a/antarest/main.py b/antarest/main.py index bda9501063..f7eb31ddb3 100644 --- a/antarest/main.py +++ b/antarest/main.py @@ -57,6 +57,8 @@ from antarest.study.storage.rawstudy.watcher import Watcher from antarest.study.web.watcher_blueprint import create_watcher_routes from antarest.tools.admin_lib import clean_locks +from antarest.worker.archive_worker import ArchiveWorker +from antarest.worker.worker import AbstractWorker logger = logging.getLogger(__name__) @@ -65,6 +67,7 @@ class Module(str, Enum): APP = "app" WATCHER = "watcher" MATRIX_GC = "matrix_gc" + ARCHIVE_WORKER = "archive_worker" def parse_arguments() -> argparse.Namespace: @@ -101,11 +104,7 @@ def parse_arguments() -> argparse.Namespace: "--module", dest="module", help="Select a module to run (default is the application server)", - choices=[ - Module.APP.value, - Module.WATCHER.value, - Module.MATRIX_GC.value, - ], + choices=[mod.value for mod in Module], action="store", default=Module.APP.value, required=False, @@ -289,6 +288,14 @@ def create_matrix_gc( ) +def create_worker( + config: Config, event_bus: Optional[IEventBus] = None +) -> AbstractWorker: + if not event_bus: + _, event_bus, _, _, _, _, _ = create_core_services(None, config) + return ArchiveWorker(event_bus, ["archive_test"]) + + def create_services( config: Config, application: Optional[FastAPI], create_all: bool = False ) -> Dict[str, Any]: @@ -551,5 +558,12 @@ def create_env(config_file: Path) -> Dict[str, Any]: init_db(config_file, config, False, None) matrix_gc = create_matrix_gc(config=config, application=None) matrix_gc.start(threaded=False) + elif module == Module.ARCHIVE_WORKER: + res = get_local_path() / "resources" + config = Config.from_yaml_file(res=res, file=config_file) + configure_logger(config) + init_db(config_file, config, False, None) + worker = create_worker(config) + worker.start() else: raise UnknownModuleError(module) diff --git a/antarest/worker/__init__.py b/antarest/worker/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/antarest/worker/archive_worker.py b/antarest/worker/archive_worker.py new file mode 100644 index 0000000000..79d715e8e6 --- /dev/null +++ b/antarest/worker/archive_worker.py @@ -0,0 +1,10 @@ +import subprocess +from typing import cast + +from antarest.core.tasks.model import TaskResult +from antarest.worker.worker import AbstractWorker, WorkerTaskCommand + + +class ArchiveWorker(AbstractWorker): + def execute_task(self, task_info: WorkerTaskCommand) -> TaskResult: + raise NotImplementedError diff --git a/antarest/worker/worker.py b/antarest/worker/worker.py new file mode 100644 index 0000000000..7ba5ca5f79 --- /dev/null +++ b/antarest/worker/worker.py @@ -0,0 +1,70 @@ +import abc +import subprocess +import time +from abc import abstractmethod +from concurrent.futures import ThreadPoolExecutor, Future +from threading import Thread +from typing import List, Dict, Union, cast + +from pydantic import BaseModel + +from antarest.core.interfaces.eventbus import IEventBus, Event, EventType +from antarest.core.tasks.model import TaskResult + +MAX_WORKERS = 10 + + +class WorkerTaskResult(BaseModel): + task_id: str + task_result: TaskResult + + +class WorkerTaskCommand(BaseModel): + task_id: str + task_type: str + task_args: Dict[str, Union[int, float, bool, str]] + + +class AbstractWorker(abc.ABC): + def __init__(self, event_bus: IEventBus, accept: List[str]) -> None: + self.event_bus = event_bus + for task_type in accept: + self.event_bus.add_queue_consumer(self.listen_for_tasks, task_type) + self.threadpool = ThreadPoolExecutor( + max_workers=MAX_WORKERS, thread_name_prefix="workertask_" + ) + self.task_watcher = Thread(target=self._loop, daemon=True) + self.futures: Dict[str, Future[TaskResult]] = {} + + def start(self, threaded: bool = False) -> None: + if threaded: + self.task_watcher.start() + else: + self._loop() + + async def listen_for_tasks(self, event: Event) -> None: + task_info = WorkerTaskCommand.parse_obj(event.payload) + self.event_bus.push( + Event(type=EventType.WORKER_TASK_STARTED, payload=task_info) + ) + self.futures[task_info.task_id] = self.threadpool.submit( + self.execute_task, task_info + ) + + @abstractmethod + def execute_task(self, task_info: WorkerTaskCommand) -> TaskResult: + raise NotImplementedError() + + def _loop(self) -> None: + while True: + for task_id, future in self.futures.items(): + if future.done(): + self.event_bus.push( + Event( + type=EventType.WORKER_TASK_ENDED, + payload=WorkerTaskResult( + task_id=task_id, task_result=future.result() + ), + ) + ) + time.sleep(2) diff --git a/tests/conftest.py b/tests/conftest.py index 4bb4e04dec..43689454c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,9 @@ import sys +import time +from datetime import datetime, timedelta from functools import wraps from pathlib import Path -from typing import Any +from typing import Any, Callable from unittest.mock import Mock import pytest @@ -75,3 +77,12 @@ def assert_study(a: SUB_JSON, b: SUB_JSON) -> None: _assert_pointer_path(a, b) else: _assert_others(a, b) + + +def autoretry_assert(func: Callable[..., bool], timeout: int) -> None: + threshold = datetime.utcnow() + timedelta(seconds=timeout) + while datetime.utcnow() < threshold: + if func(): + return + time.sleep(0.2) + raise AssertionError() diff --git a/tests/core/test_tasks.py b/tests/core/test_tasks.py index d99cf45e97..4c1199d707 100644 --- a/tests/core/test_tasks.py +++ b/tests/core/test_tasks.py @@ -1,12 +1,13 @@ import datetime -from typing import Callable +from pathlib import Path +from typing import Callable, List from unittest.mock import Mock, ANY, call import pytest from sqlalchemy import create_engine from antarest.core.config import Config -from antarest.core.interfaces.eventbus import EventType, Event +from antarest.core.interfaces.eventbus import EventType, Event, IEventBus from antarest.core.jwt import DEFAULT_ADMIN_USER from antarest.core.persistence import Base from antarest.core.requests import RequestParameters, UserHasNotPermissionError @@ -22,9 +23,13 @@ from antarest.core.tasks.repository import TaskJobRepository from antarest.core.tasks.service import TaskJobService from antarest.core.utils.fastapi_sqlalchemy import DBSessionMiddleware, db +from antarest.eventbus.business.local_eventbus import LocalEventBus +from antarest.eventbus.service import EventBusService +from antarest.worker.worker import AbstractWorker, WorkerTaskCommand +from tests.conftest import with_db_context -def test_service() -> TaskJobService: +def test_service() -> None: engine = create_engine("sqlite:///:memory:", echo=True) Base.metadata.create_all(engine) DBSessionMiddleware( @@ -256,7 +261,78 @@ def action_ok(update_msg: Callable[[str], None]) -> TaskResult: service.await_task("elsewhere") repo_mock.get.assert_called_with("elsewhere") - return service + +class DummyWorker(AbstractWorker): + def __init__( + self, event_bus: IEventBus, accept: List[str], tmp_path: Path + ): + super().__init__(event_bus, accept) + self.tmp_path = tmp_path + + def execute_task(self, task_info: WorkerTaskCommand) -> TaskResult: + relative_path = task_info.task_args["file"] + (self.tmp_path / relative_path).touch() + return TaskResult(success=True, message="") + + +@with_db_context +def test_worker_tasks(tmp_path: Path): + repo_mock = Mock(spec=TaskJobRepository) + repo_mock.list.return_value = [] + event_bus = EventBusService(LocalEventBus()) + service = TaskJobService( + config=Config(), repository=repo_mock, event_bus=event_bus + ) + + worker = DummyWorker(event_bus, ["test"], tmp_path) + worker.start(threaded=True) + + file_to_create = "foo" + + assert not (tmp_path / file_to_create).exists() + + repo_mock.save.side_effect = [ + TaskJob( + id="taskid", + name="Unnamed", + owner_id=0, + type=TaskType.WORKER_TASK, + ref_id=None, + ), + TaskJob( + id="taskid", + name="Unnamed", + owner_id=0, + type=TaskType.WORKER_TASK, + ref_id=None, + status=TaskStatus.RUNNING, + ), + TaskJob( + id="taskid", + name="Unnamed", + owner_id=0, + type=TaskType.WORKER_TASK, + ref_id=None, + status=TaskStatus.COMPLETED, + ), + ] + repo_mock.get_or_raise.return_value = TaskJob( + id="taskid", + name="Unnamed", + owner_id=0, + type=TaskType.WORKER_TASK, + ref_id=None, + ) + task_id = service.add_worker_task( + "test", + {"file": file_to_create}, + None, + None, + request_params=RequestParameters(user=DEFAULT_ADMIN_USER), + ) + service.await_task(task_id) + + assert (tmp_path / file_to_create).exists() def test_repository(): diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 8843c9ca96..a8a66dd74a 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -3,7 +3,7 @@ import pytest from antarest.core.exceptions import ShouldNotHappenException -from antarest.core.utils.utils import retry, concat_files +from antarest.core.utils.utils import retry, concat_files, suppress_exception def test_retry(): @@ -24,3 +24,12 @@ def test_concat_files(tmp_path: Path): f3.write_text("Done.") concat_files([f1, f2, f3], f_target) assert f_target.read_text(encoding="utf-8") == "hello world !\nDone." + + +def test_suppress_exception(): + def func_failure() -> str: + raise ShouldNotHappenException() + + catched_exc = [] + suppress_exception(func_failure, lambda ex: catched_exc.append(ex)) + assert len(catched_exc) == 1 diff --git a/tests/eventbus/test_local_eventbus.py b/tests/eventbus/test_local_eventbus.py index 7752d89d70..5667a83ba6 100644 --- a/tests/eventbus/test_local_eventbus.py +++ b/tests/eventbus/test_local_eventbus.py @@ -1,10 +1,10 @@ -from antarest.core.interfaces.eventbus import Event +from antarest.core.interfaces.eventbus import Event, EventType from antarest.eventbus.business.local_eventbus import LocalEventBus def test_lifecycle(): eventbus = LocalEventBus() - event = Event(type="test", payload="foo") + event = Event(type=EventType.STUDY_EDITED, payload="foo") eventbus.push_event(event) assert eventbus.get_events() == [event] eventbus.clear_events() diff --git a/tests/eventbus/test_redis_event_bus.py b/tests/eventbus/test_redis_event_bus.py index dba0bd6758..a7a45829a2 100644 --- a/tests/eventbus/test_redis_event_bus.py +++ b/tests/eventbus/test_redis_event_bus.py @@ -2,7 +2,7 @@ import json from unittest.mock import Mock -from antarest.core.interfaces.eventbus import Event +from antarest.core.interfaces.eventbus import Event, EventType from antarest.eventbus.business.redis_eventbus import ( RedisEventBus, ) @@ -15,7 +15,7 @@ def test_lifecycle(): eventbus = RedisEventBus(redis_client) pubsub_mock.subscribe.assert_called_once_with("events") - event = Event(type="test", payload="foo") + event = Event(type=EventType.STUDY_EDITED, payload="foo") serialized = event.json() pubsub_mock.get_message.return_value = {"data": serialized} eventbus.push_event(event) diff --git a/tests/eventbus/test_service.py b/tests/eventbus/test_service.py index bbad16685a..9052b14249 100644 --- a/tests/eventbus/test_service.py +++ b/tests/eventbus/test_service.py @@ -1,20 +1,13 @@ +import asyncio import time from datetime import datetime, timedelta -from typing import Callable +from typing import Callable, List, Awaitable from unittest.mock import Mock, MagicMock from antarest.core.config import Config, EventBusConfig, RedisConfig -from antarest.core.interfaces.eventbus import Event +from antarest.core.interfaces.eventbus import Event, EventType from antarest.eventbus.main import build_eventbus - - -def autoretry(func: Callable[..., bool], timeout: int) -> None: - threshold = datetime.utcnow() + timedelta(seconds=timeout) - while datetime.utcnow() < threshold: - if func(): - return - time.sleep(0.2) - raise AssertionError() +from tests.conftest import autoretry_assert def test_service_factory(): @@ -34,13 +27,36 @@ def test_service_factory(): def test_lifecycle(): event_bus = build_eventbus(MagicMock(), Config(), autostart=True) - test_bucket = [] - lid = event_bus.add_listener(lambda event: test_bucket.append(event)) - event = Event(type="test", payload="foo") - event_bus.push(event) - autoretry(lambda: len(test_bucket) == 1, 2) + test_bucket: List[Event] = [] + + def append_to_bucket( + bucket: List[Event], + ) -> Callable[[Event], Awaitable[None]]: + async def _append_to_bucket(event: Event): + bucket.append(event) - event_bus.remove_listener(lid) + return _append_to_bucket + + lid1 = event_bus.add_listener(append_to_bucket(test_bucket)) + lid2 = event_bus.add_listener( + append_to_bucket(test_bucket), [EventType.STUDY_CREATED] + ) + event_bus.push(Event(type=EventType.STUDY_JOB_STARTED, payload="foo")) + event_bus.push(Event(type=EventType.STUDY_CREATED, payload="foo")) + autoretry_assert(lambda: len(test_bucket) == 3, 2) + + event_bus.remove_listener(lid1) + event_bus.remove_listener(lid2) test_bucket.clear() - event_bus.push(event) - autoretry(lambda: len(test_bucket) == 0, 2) + event_bus.push(Event(type=EventType.STUDY_JOB_STARTED, payload="foo")) + autoretry_assert(lambda: len(test_bucket) == 0, 2) + + queue_name = "some work job" + event_bus.add_queue_consumer(append_to_bucket(test_bucket), queue_name) + event_bus.add_queue_consumer( + lambda event: test_bucket.append(event), queue_name + ) + event_bus.queue( + Event(type=EventType.WORKER_TASK, payload="worker task"), queue_name + ) + autoretry_assert(lambda: len(test_bucket) == 1, 2) diff --git a/tests/storage/conftest.py b/tests/storage/conftest.py index 9aea4b737f..2bfd3e1d9c 100644 --- a/tests/storage/conftest.py +++ b/tests/storage/conftest.py @@ -3,7 +3,7 @@ import sys import uuid from pathlib import Path -from typing import Callable, Optional, List +from typing import Callable, Optional, List, Dict, Union from unittest.mock import Mock import pytest @@ -293,6 +293,16 @@ def notifier(message: str): return notifier + def add_worker_task( + self, + task_type: str, + task_args: Dict[str, Union[int, float, bool, str]], + name: Optional[str], + ref_id: Optional[str], + request_params: RequestParameters, + ) -> str: + raise NotImplementedError() + def add_task( self, action: Task, diff --git a/tests/worker/__init__.py b/tests/worker/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py new file mode 100644 index 0000000000..dba50f8368 --- /dev/null +++ b/tests/worker/test_worker.py @@ -0,0 +1,46 @@ +from pathlib import Path +from typing import List +from unittest.mock import MagicMock + +from antarest.core.config import Config +from antarest.core.interfaces.eventbus import IEventBus, Event, EventType +from antarest.core.tasks.model import TaskResult +from antarest.eventbus.main import build_eventbus +from antarest.worker.worker import AbstractWorker, WorkerTaskCommand +from tests.conftest import autoretry_assert + + +class DummyWorker(AbstractWorker): + def __init__( + self, event_bus: IEventBus, accept: List[str], tmp_path: Path + ): + super().__init__(event_bus, accept) + self.tmp_path = tmp_path + + def execute_task(self, task_info: WorkerTaskCommand) -> TaskResult: + relative_path = task_info.task_args["file"] + (self.tmp_path / relative_path).touch() + return TaskResult(success=True, message="") + + +def test_simple_task(tmp_path: Path): + task_queue = "do_stuff" + event_bus = build_eventbus(MagicMock(), Config(), autostart=True) + event_bus.queue( + Event( + type=EventType.WORKER_TASK, + payload=WorkerTaskCommand( + task_type="touch stuff", + task_id="some task", + task_args={"file": "foo"}, + ), + ), + task_queue, + ) + + assert not (tmp_path / "foo").exists() + + worker = DummyWorker(event_bus, [task_queue], tmp_path) + worker.start(threaded=True) + + autoretry_assert(lambda: (tmp_path / "foo").exists(), 2) From 7475f09f45c59f2debc9d293d710a153f77575fa Mon Sep 17 00:00:00 2001 From: Charly Bion <37449809+Hyralc@users.noreply.github.com> Date: Wed, 6 Jul 2022 15:55:51 +0200 Subject: [PATCH 21/24] Enable reading from zipped files (#961) --- antarest/core/exceptions.py | 5 + antarest/launcher/service.py | 10 +- antarest/study/common/utils.py | 15 ++ antarest/study/service.py | 22 +-- .../study/storage/abstract_storage_service.py | 18 +-- .../rawstudy/model/filesystem/bucket_node.py | 4 +- .../rawstudy/model/filesystem/config/files.py | 151 ++++++++++++++---- .../rawstudy/model/filesystem/config/model.py | 15 +- .../rawstudy/model/filesystem/folder_node.py | 8 +- .../model/filesystem/ini_file_node.py | 22 ++- .../rawstudy/model/filesystem/inode.py | 38 ++++- .../rawstudy/model/filesystem/lazy_node.py | 15 +- .../filesystem/matrix/input_series_matrix.py | 11 +- .../model/filesystem/matrix/matrix.py | 20 ++- .../filesystem/matrix/output_series_matrix.py | 31 +++- .../model/filesystem/raw_file_node.py | 16 +- .../model/filesystem/root/input/areas/list.py | 9 +- .../model/filesystem/root/output/output.py | 7 +- .../simulation/ts_numbers/ts_numbers_data.py | 16 +- .../study/storage/rawstudy/model/helpers.py | 46 +----- .../storage/rawstudy/raw_study_service.py | 25 ++- antarest/study/storage/rawstudy/watcher.py | 4 +- antarest/study/storage/utils.py | 2 +- examples/studies/STA-mini.zip | Bin 467304 -> 445596 bytes tests/integration/test_integration.py | 2 +- .../filesystem/matrix/test_matrix_node.py | 3 + .../repository/filesystem/test_folder_node.py | 1 + tests/storage/test_model.py | 4 +- .../HomeView/InformationView/index.tsx | 28 +++- .../components/App/Singlestudy/NavHeader.tsx | 8 +- .../explore/Modelization/DebugView/index.tsx | 2 +- .../src/components/App/Studies/StudyCard.tsx | 18 +-- 32 files changed, 391 insertions(+), 185 deletions(-) create mode 100644 antarest/study/common/utils.py diff --git a/antarest/core/exceptions.py b/antarest/core/exceptions.py index 83fb6da848..a65e882e4c 100644 --- a/antarest/core/exceptions.py +++ b/antarest/core/exceptions.py @@ -126,5 +126,10 @@ def __init__(self, message: str) -> None: super().__init__(HTTPStatus.NOT_FOUND, message) +class WritingInsideZippedFileException(HTTPException): + def __init__(self, message: str) -> None: + super().__init__(HTTPStatus.BAD_REQUEST, message) + + class StudyOutputNotFoundError(Exception): pass diff --git a/antarest/launcher/service.py b/antarest/launcher/service.py index 21e3412713..cd5864f797 100644 --- a/antarest/launcher/service.py +++ b/antarest/launcher/service.py @@ -1,4 +1,3 @@ -import json import logging import os import shutil @@ -24,7 +23,6 @@ from antarest.core.jwt import JWTUser, DEFAULT_ADMIN_USER from antarest.core.model import ( StudyPermissionType, - JSON, ) from antarest.core.requests import ( RequestParameters, @@ -54,7 +52,6 @@ assert_permission, create_permission_from_study, extract_output_name, - fix_study_root, find_single_output_path, ) @@ -404,11 +401,10 @@ def get_log( job_result = self.job_result_repository.get(str(job_id)) if job_result: - # TODO: remove this part of code when study tree zipfile support is implemented launcher_parameters = LauncherParametersDTO.parse_raw( job_result.launcher_params or "{}" ) - if job_result.output_id and not launcher_parameters.archive_output: + if job_result.output_id: if log_type == LogType.STDOUT: launcher_logs = cast( bytes, @@ -574,7 +570,9 @@ def _import_output( output_true_path.parent / f"{output_true_path.name}.zip" ) - zip_dir(output_true_path, zip_path=zip_path) + zip_dir( + output_true_path, zip_path=zip_path + ) # TODO: remove source dir ? stopwatch.log_elapsed( lambda x: logger.info( f"Zipped output for job {job_id} in {x}s" diff --git a/antarest/study/common/utils.py b/antarest/study/common/utils.py new file mode 100644 index 0000000000..9993ac7d82 --- /dev/null +++ b/antarest/study/common/utils.py @@ -0,0 +1,15 @@ +import tempfile +from pathlib import Path +from typing import Tuple, Any +from zipfile import ZipFile + + +def extract_file_to_tmp_dir( + zip_path: Path, inside_zip_path: Path +) -> Tuple[Path, Any]: + str_inside_zip_path = str(inside_zip_path).replace("\\", "/") + tmp_dir = tempfile.TemporaryDirectory() + with ZipFile(zip_path) as zip_obj: + zip_obj.extract(str_inside_zip_path, tmp_dir.name) + path = Path(tmp_dir.name) / inside_zip_path + return path, tmp_dir diff --git a/antarest/study/service.py b/antarest/study/service.py index 67419f6e79..eda37362d3 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -47,12 +47,10 @@ TaskUpdateNotifier, noop_notifier, ) -from antarest.core.utils.utils import concat_files, StopWatch +from antarest.core.utils.utils import StopWatch from antarest.login.model import Group from antarest.login.service import LoginService from antarest.matrixstore.business.matrix_editor import ( - Operation, - MatrixSlice, MatrixEditInstructionDTO, ) from antarest.matrixstore.utils import parse_tsv_matrix @@ -218,7 +216,6 @@ def get( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.storage_service.get_storage(study).get( study, url, depth, formatted ) @@ -239,7 +236,6 @@ def get_comments( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) output: Union[str, JSON] if isinstance(study, RawStudy): output = self.storage_service.get_storage(study).get( @@ -1206,7 +1202,6 @@ def get_study_sim_result( """ study = self.get_study(study_id) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) logger.info( "study %s output listing asked by user %s", study_id, @@ -1668,7 +1663,6 @@ def get_all_areas( ) -> Union[List[AreaInfoDTO], Dict[str, Any]]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return ( self.areas.get_all_areas_ui_info(study) if ui @@ -1683,7 +1677,6 @@ def get_all_links( ) -> List[LinkInfoDTO]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.links.get_all_links(study, with_ui) def create_area( @@ -1832,7 +1825,9 @@ def archive(self, uuid: str, params: RequestParameters) -> str: def archive_task(notifier: TaskUpdateNotifier) -> TaskResult: study_to_archive = self.get_study(uuid) - self.storage_service.raw_study_service.archive(study_to_archive) + archived_path = self.storage_service.raw_study_service.archive( + study_to_archive + ) study_to_archive.archived = True self.repository.save(study_to_archive) self.event_bus.push( @@ -1889,6 +1884,7 @@ def unarchive_task(notifier: TaskUpdateNotifier) -> TaskResult: study_to_archive, io.BytesIO(fh.read()) ) study_to_archive.archived = False + os.unlink( self.storage_service.raw_study_service.get_archive_path( study_to_archive @@ -1902,6 +1898,7 @@ def unarchive_task(notifier: TaskUpdateNotifier) -> TaskResult: permissions=create_permission_from_study(study), ) ) + remove_from_cache(cache=self.cache_service, root_id=uuid) return TaskResult(success=True, message="ok") return self.task_service.add_task( @@ -2035,7 +2032,6 @@ def get_xpansion_settings( ) -> XpansionSettingsDTO: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_xpansion_settings(study) def update_xpansion_settings( @@ -2069,7 +2065,6 @@ def get_candidate( ) -> XpansionCandidateDTO: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_candidate(study, candidate_name) def get_candidates( @@ -2077,7 +2072,6 @@ def get_candidates( ) -> List[XpansionCandidateDTO]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_candidates(study) def update_xpansion_candidate( @@ -2141,7 +2135,6 @@ def get_single_xpansion_constraints( ) -> bytes: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_single_xpansion_constraints( study, filename ) @@ -2151,7 +2144,6 @@ def get_all_xpansion_constraints( ) -> List[str]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_all_xpansion_constraints(study) def add_capa( @@ -2175,14 +2167,12 @@ def get_single_capa( ) -> JSON: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_single_capa(study, filename) def get_all_capa(self, uuid: str, params: RequestParameters) -> List[str]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_all_capa(study) def update_matrix( diff --git a/antarest/study/storage/abstract_storage_service.py b/antarest/study/storage/abstract_storage_service.py index 0458f6ec22..68f35b428e 100644 --- a/antarest/study/storage/abstract_storage_service.py +++ b/antarest/study/storage/abstract_storage_service.py @@ -274,19 +274,13 @@ def import_output( Path(path_output.parent, output_full_name + extension) ) - if not is_zipped: - data = self.get( - metadata, f"output/{output_full_name}", -1, use_cache=False - ) + data = self.get( + metadata, f"output/{output_full_name}", -1, use_cache=False + ) - if data is None: - self.delete_output(metadata, "imported_output") - raise BadOutputError("The output provided is not conform.") - else: - # TODO: remove this part of code when study tree zipfile support is implemented - logger.warning( - "The imported output is zipped: no check is done" - ) + if data is None: + self.delete_output(metadata, "imported_output") + raise BadOutputError("The output provided is not conform.") except Exception as e: logger.error("Failed to import output", exc_info=e) diff --git a/antarest/study/storage/rawstudy/model/filesystem/bucket_node.py b/antarest/study/storage/rawstudy/model/filesystem/bucket_node.py index 6115bd358d..f3c5413380 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/bucket_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/bucket_node.py @@ -1,7 +1,6 @@ -from typing import Optional, List, Union, Dict, Callable, Any +from typing import Optional, List, Dict, Callable, Any from antarest.core.model import JSON, SUB_JSON -from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.model.filesystem.config.model import ( FileStudyTreeConfig, ) @@ -62,6 +61,7 @@ def save( data: SUB_JSON, url: Optional[List[str]] = None, ) -> None: + self._assert_not_in_zipped_file() if not self.config.path.exists(): self.config.path.mkdir() diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/files.py b/antarest/study/storage/rawstudy/model/filesystem/config/files.py index dc1581841c..d0afc76fbf 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/files.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/files.py @@ -1,11 +1,13 @@ import logging import re import tempfile +from enum import Enum from pathlib import Path from typing import List, Dict, Any, Tuple, Optional, cast from zipfile import ZipFile from antarest.core.model import JSON +from antarest.study.common.utils import extract_file_to_tmp_dir from antarest.study.storage.rawstudy.io.reader import ( IniReader, MultipleSameKeysIniReader, @@ -27,6 +29,16 @@ logger = logging.getLogger(__name__) +class FileType(Enum): + TXT = "txt" + SIMPLE_INI = "simple_ini" + MULTI_INI = "multi_ini" + + +class FileTypeNotSupportedException(Exception): + pass + + class ConfigPathBuilder: """ Fetch information need by StudyConfig from filesystem data @@ -50,10 +62,12 @@ def build( study_path ) + study_path_without_zip_extension = study_path.parent / study_path.stem + return FileStudyTreeConfig( study_path=study_path, output_path=output_path or study_path / "output", - path=study_path, + path=study_path_without_zip_extension, study_id=study_id, version=ConfigPathBuilder._parse_version(study_path), areas=ConfigPathBuilder._parse_areas(study_path), @@ -65,19 +79,59 @@ def build( store_new_set=sns, archive_input_series=asi, enr_modelling=enr_modelling, + zip_path=study_path if study_path.suffix == ".zip" else None, ) + @staticmethod + def _extract_data_from_file( + root: Path, + inside_root_path: Path, + file_type: FileType, + multi_ini_keys: Optional[List[str]] = None, + ) -> Any: + tmp_dir = None + if root.suffix == ".zip": + output_data_path, tmp_dir = extract_file_to_tmp_dir( + root, inside_root_path + ) + else: + output_data_path = root / inside_root_path + + try: + if file_type == FileType.TXT: + output_data: Any = output_data_path.read_text().split("\n") + elif file_type == FileType.MULTI_INI: + output_data = MultipleSameKeysIniReader(multi_ini_keys).read( + output_data_path + ) + elif file_type == FileType.SIMPLE_INI: + output_data = IniReader().read(output_data_path) + else: + raise FileTypeNotSupportedException() + finally: + if tmp_dir: + tmp_dir.cleanup() + + return output_data + @staticmethod def _parse_version(path: Path) -> int: - studyinfo = IniReader().read(path / "study.antares") - version: int = studyinfo.get("antares", {}).get("version", -1) + study_info = ConfigPathBuilder._extract_data_from_file( + root=path, + inside_root_path=Path("study.antares"), + file_type=FileType.SIMPLE_INI, + ) + version: int = study_info.get("antares", {}).get("version", -1) return version @staticmethod def _parse_parameters(path: Path) -> Tuple[bool, List[str], str]: - general = MultipleSameKeysIniReader().read( - path / "settings/generaldata.ini" + general = ConfigPathBuilder._extract_data_from_file( + root=path, + inside_root_path=Path("settings/generaldata.ini"), + file_type=FileType.MULTI_INI, ) + store_new_set: bool = general.get("output", {}).get( "storenewset", False ) @@ -96,8 +150,12 @@ def _parse_parameters(path: Path) -> Tuple[bool, List[str], str]: @staticmethod def _parse_bindings(root: Path) -> List[BindingConstraintDTO]: - bindings = IniReader().read( - root / "input/bindingconstraints/bindingconstraints.ini" + bindings = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path( + "input/bindingconstraints/bindingconstraints.ini" + ), + file_type=FileType.SIMPLE_INI, ) output_list = [] for id, bind in bindings.items(): @@ -124,8 +182,11 @@ def _parse_bindings(root: Path) -> List[BindingConstraintDTO]: @staticmethod def _parse_sets(root: Path) -> Dict[str, DistrictSet]: - json = MultipleSameKeysIniReader(["+", "-"]).read( - root / "input/areas/sets.ini" + json = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path("input/areas/sets.ini"), + file_type=FileType.MULTI_INI, + multi_ini_keys=["+", "-"], ) return { name.lower(): DistrictSet( @@ -144,7 +205,11 @@ def _parse_sets(root: Path) -> Dict[str, DistrictSet]: @staticmethod def _parse_areas(root: Path) -> Dict[str, Area]: - areas = (root / "input/areas/list.txt").read_text().split("\n") + areas = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path("input/areas/list.txt"), + file_type=FileType.TXT, + ) areas = [a for a in areas if a != ""] return { transform_name_to_id(a): ConfigPathBuilder.parse_area(root, a) @@ -171,6 +236,11 @@ def parse_simulation(path: Path) -> Optional["Simulation"]: "^([0-9]{8}-[0-9]{4})(eco|adq)-?(.*)", path.stem ) try: + if path.suffix == ".zip": + zf = ZipFile(path, "r") + error = str("checkIntegrity.txt") not in zf.namelist() + else: + error = not (path / "checkIntegrity.txt").exists() ( nbyears, by_year, @@ -184,7 +254,7 @@ def parse_simulation(path: Path) -> Optional["Simulation"]: nbyears=nbyears, by_year=by_year, synthesis=synthesis, - error=not (path / "checkIntegrity.txt").exists(), + error=error, playlist=playlist, archived=path.suffix == ".zip", ) @@ -259,8 +329,10 @@ def parse_area(root: Path, area: str) -> "Area": @staticmethod def _parse_thermal(root: Path, area: str) -> List[Cluster]: - list_ini = IniReader().read( - root / f"input/thermal/clusters/{area}/list.ini" + list_ini = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path(f"input/thermal/clusters/{area}/list.ini"), + file_type=FileType.SIMPLE_INI, ) return [ Cluster( @@ -273,24 +345,31 @@ def _parse_thermal(root: Path, area: str) -> List[Cluster]: @staticmethod def _parse_renewables(root: Path, area: str) -> List[Cluster]: - ini_path = root / f"input/renewables/clusters/{area}/list.ini" - if not ini_path.exists(): - return [] - - list_ini = IniReader().read(ini_path) - return [ - Cluster( - id=transform_name_to_id(key), - enabled=list_ini.get(key, {}).get("enabled", True), - name=list_ini.get(key, {}).get("name", None), + try: + list_ini = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path( + f"input/renewables/clusters/{area}/list.ini" + ), + file_type=FileType.SIMPLE_INI, ) - for key in list(list_ini.keys()) - ] + return [ + Cluster( + id=transform_name_to_id(key), + enabled=list_ini.get(key, {}).get("enabled", True), + name=list_ini.get(key, {}).get("name", None), + ) + for key in list(list_ini.keys()) + ] + except: + return [] @staticmethod def _parse_links(root: Path, area: str) -> Dict[str, Link]: - properties_ini = IniReader().read( - root / f"input/links/{area}/properties.ini" + properties_ini = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path(f"input/links/{area}/properties.ini"), + file_type=FileType.SIMPLE_INI, ) return { link: Link.from_json(properties_ini[link]) @@ -299,14 +378,20 @@ def _parse_links(root: Path, area: str) -> Dict[str, Link]: @staticmethod def _parse_filters_synthesis(root: Path, area: str) -> List[str]: - filters: str = IniReader().read( - root / f"input/areas/{area}/optimization.ini" - )["filtering"]["filter-synthesis"] + optimization = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path(f"input/areas/{area}/optimization.ini"), + file_type=FileType.SIMPLE_INI, + ) + filters: str = optimization["filtering"]["filter-synthesis"] return Link.split(filters) @staticmethod def _parse_filters_year(root: Path, area: str) -> List[str]: - filters: str = IniReader().read( - root / f"input/areas/{area}/optimization.ini" - )["filtering"]["filter-year-by-year"] + optimization = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path(f"input/areas/{area}/optimization.ini"), + file_type=FileType.SIMPLE_INI, + ) + filters: str = optimization["filtering"]["filter-year-by-year"] return Link.split(filters) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/model.py b/antarest/study/storage/rawstudy/model/filesystem/config/model.py index 8553830d5e..af4f118e3d 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/model.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/model.py @@ -124,6 +124,7 @@ def __init__( archive_input_series: Optional[List[str]] = None, enr_modelling: str = ENR_MODELLING.AGGREGATED.value, cache: Optional[Dict[str, List[str]]] = None, + zip_path: Optional[Path] = None, ): self.study_path = study_path self.path = path @@ -138,8 +139,16 @@ def __init__( self.archive_input_series = archive_input_series or list() self.enr_modelling = enr_modelling self.cache = cache or dict() + self.zip_path = zip_path + + def next_file( + self, name: str, is_output: bool = False + ) -> "FileStudyTreeConfig": + if is_output and name in self.outputs and self.outputs[name].archived: + zip_path: Optional[Path] = self.path / f"{name}.zip" + else: + zip_path = self.zip_path - def next_file(self, name: str) -> "FileStudyTreeConfig": return FileStudyTreeConfig( study_path=self.study_path, output_path=self.output_path, @@ -154,6 +163,7 @@ def next_file(self, name: str) -> "FileStudyTreeConfig": archive_input_series=self.archive_input_series, enr_modelling=self.enr_modelling, cache=self.cache, + zip_path=zip_path, ) def at_file(self, filepath: Path) -> "FileStudyTreeConfig": @@ -276,6 +286,7 @@ class FileStudyTreeConfigDTO(BaseModel): store_new_set: bool = False archive_input_series: List[str] = list() enr_modelling: str = ENR_MODELLING.AGGREGATED.value + zip_path: Optional[Path] = None @staticmethod def from_build_config( @@ -294,6 +305,7 @@ def from_build_config( store_new_set=config.store_new_set, archive_input_series=config.archive_input_series, enr_modelling=config.enr_modelling, + zip_path=config.zip_path, ) def to_build_config(self) -> FileStudyTreeConfig: @@ -310,4 +322,5 @@ def to_build_config(self) -> FileStudyTreeConfig: store_new_set=self.store_new_set, archive_input_series=self.archive_input_series, enr_modelling=self.enr_modelling, + zip_path=self.zip_path, ) diff --git a/antarest/study/storage/rawstudy/model/filesystem/folder_node.py b/antarest/study/storage/rawstudy/model/filesystem/folder_node.py index 373ce7b311..8e60c1f3d9 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/folder_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/folder_node.py @@ -6,7 +6,6 @@ from fastapi import HTTPException from antarest.core.model import JSON, SUB_JSON -from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.model.filesystem.config.model import ( FileStudyTreeConfig, ) @@ -32,7 +31,7 @@ class FolderNode(INode[JSON, SUB_JSON, JSON], ABC): """ Hub node which forward request deeper in tree according to url. Or expand request according to depth. Its children is set node by node following antares tree structure. - Strucuture is implemented in antarest.study.repository.filesystem.root + Structure is implemented in antarest.study.repository.filesystem.root """ def __init__( @@ -143,6 +142,7 @@ def save( (name,), sub_url = self.extract_child(children, url) return children[name].save(data, sub_url) else: + self._assert_not_in_zipped_file() if not self.config.path.exists(): self.config.path.mkdir() assert isinstance(data, Dict) @@ -198,13 +198,13 @@ def extract_child( names = list(children.keys()) if names[0] == "*" else names if names[0] not in children: raise ChildNotFoundError( - f"{names[0]} not a children of {self.__class__.__name__}" + f"{names[0]} not a child of {self.__class__.__name__}" ) child_class = type(children[names[0]]) for name in names: if name not in children: raise ChildNotFoundError( - f"{name} not a children of {self.__class__.__name__}" + f"{name} not a child of {self.__class__.__name__}" ) if type(children[name]) != child_class: raise FilterError("Filter selection has different classes") diff --git a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py index 35784fcaaf..417c559379 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py @@ -6,7 +6,6 @@ from filelock import FileLock from antarest.core.model import JSON, SUB_JSON -from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.io.reader import IniReader from antarest.study.storage.rawstudy.io.reader.ini_reader import IReader from antarest.study.storage.rawstudy.io.writer.ini_writer import ( @@ -20,7 +19,6 @@ ) from antarest.study.storage.rawstudy.model.filesystem.inode import ( INode, - TREE, ) @@ -65,10 +63,21 @@ def _get( if depth == 0: return {} url = url or [] - try: - json = self.reader.read(self.path) - except Exception as e: - raise IniReaderError(self.__class__.__name__, str(e)) + + if self.config.zip_path: + file_path, tmp_dir = self._extract_file_to_tmp_dir() + try: + json = self.reader.read(file_path) + except Exception as e: + raise IniReaderError(self.__class__.__name__, str(e)) + finally: + tmp_dir.cleanup() + else: + try: + json = self.reader.read(self.path) + except Exception as e: + raise IniReaderError(self.__class__.__name__, str(e)) + if len(url) == 2: json = json[url[0]][url[1]] elif len(url) == 1: @@ -97,6 +106,7 @@ def get_node( return output def save(self, data: SUB_JSON, url: Optional[List[str]] = None) -> None: + self._assert_not_in_zipped_file() url = url or [] with FileLock( str( diff --git a/antarest/study/storage/rawstudy/model/filesystem/inode.py b/antarest/study/storage/rawstudy/model/filesystem/inode.py index c11512fd3d..f42a7b2e51 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/inode.py +++ b/antarest/study/storage/rawstudy/model/filesystem/inode.py @@ -1,6 +1,12 @@ from abc import ABC, abstractmethod -from typing import List, Optional, Dict, TypeVar, Generic, Any +from pathlib import Path +from typing import List, Optional, Dict, TypeVar, Generic, Any, Tuple +from antarest.core.exceptions import ( + ShouldNotHappenException, + WritingInsideZippedFileException, +) +from antarest.study.common.utils import extract_file_to_tmp_dir from antarest.study.storage.rawstudy.model.filesystem.config.model import ( FileStudyTreeConfig, ) @@ -128,5 +134,35 @@ def _assert_url_end(self, url: Optional[List[str]] = None) -> None: f"url should be fully resolved when arrives on {self.__class__.__name__}" ) + def _extract_file_to_tmp_dir( + self, + ) -> Tuple[Path, Any]: + """ + Happens when the file is inside an archive (aka self.config.zip_file is set) + Unzip the file into a temporary directory. + + Returns: + The actual path of the extracted file + the tmp_dir object which MUST be cleared after use of the file + """ + if self.config.zip_path is None: + raise ShouldNotHappenException() + inside_zip_path = str(self.config.path)[ + len(str(self.config.zip_path)[:-4]) + 1 : + ] + if self.config.zip_path: + return extract_file_to_tmp_dir( + self.config.zip_path, Path(inside_zip_path) + ) + else: + raise ShouldNotHappenException() + + def _assert_not_in_zipped_file(self) -> None: + """Prevents writing inside a zip file""" + if self.config.zip_path: + raise WritingInsideZippedFileException( + "Trying to save inside a zipped file" + ) + TREE = Dict[str, INode[Any, Any, Any]] diff --git a/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py b/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py index b831c88599..cff6ca60bc 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py @@ -1,8 +1,7 @@ from abc import ABC, abstractmethod from pathlib import Path -from typing import Optional, List, Generic, Union, cast +from typing import Optional, List, Generic, Union, cast, Tuple, Any -from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.model.filesystem.config.model import ( FileStudyTreeConfig, ) @@ -30,6 +29,17 @@ def __init__( self.context = context super().__init__(config) + def _get_real_file_path( + self, + ) -> Tuple[Path, Any]: + tmp_dir = None + if self.config.zip_path: + path, tmp_dir = self._extract_file_to_tmp_dir() + + else: + path = self.config.path + return path, tmp_dir + def _get( self, url: Optional[List[str]] = None, @@ -88,6 +98,7 @@ def get_link_path(self) -> Path: def save( self, data: Union[str, bytes, S], url: Optional[List[str]] = None ) -> None: + self._assert_not_in_zipped_file() self._assert_url_end(url) if isinstance(data, str) and self.context.resolver.resolve(data): diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py index a3209b3a74..ead6b0b82c 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py @@ -1,5 +1,6 @@ import logging -from typing import List, Optional +from pathlib import Path +from typing import List, Optional, Any import pandas as pd # type: ignore from pandas.errors import EmptyDataError # type: ignore @@ -12,7 +13,6 @@ from antarest.study.storage.rawstudy.model.filesystem.context import ( ContextServer, ) -from antarest.study.storage.rawstudy.model.filesystem.inode import TREE from antarest.study.storage.rawstudy.model.filesystem.matrix.matrix import ( MatrixNode, ) @@ -37,11 +37,14 @@ def __init__( def parse( self, + file_path: Optional[Path] = None, + tmp_dir: Any = None, ) -> JSON: + file_path = file_path or self.config.path try: stopwatch = StopWatch() matrix: pd.DataFrame = pd.read_csv( - self.config.path, + file_path, sep="\t", dtype=float, header=None, @@ -58,7 +61,7 @@ def parse( return data except EmptyDataError: - logger.warning(f"Empty file found when parsing {self.config.path}") + logger.warning(f"Empty file found when parsing {file_path}") return {} def _dump_json(self, data: JSON) -> None: diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py index 3bb3ba144e..de170b8d29 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py @@ -1,6 +1,7 @@ import logging from abc import ABC, abstractmethod -from typing import List, Optional, Union +from pathlib import Path +from typing import List, Optional, Union, Any from antarest.core.model import JSON from antarest.study.storage.rawstudy.model.filesystem.config.model import ( @@ -38,7 +39,7 @@ def get_lazy_content( return f"matrixfile://{self.config.path.name}" def normalize(self) -> None: - if self.get_link_path().exists(): + if self.get_link_path().exists() or self.config.zip_path: return matrix = self.parse() @@ -71,17 +72,24 @@ def load( expanded: bool = False, formatted: bool = True, ) -> Union[bytes, JSON]: + file_path, tmp_dir = self._get_real_file_path() if not formatted: - if self.config.path.exists(): - return self.config.path.read_bytes() + if file_path.exists(): + return file_path.read_bytes() logger.warning(f"Missing file {self.config.path}") + if tmp_dir: + tmp_dir.cleanup() return b"" - return self.parse() + return self.parse(file_path, tmp_dir) @abstractmethod - def parse(self) -> JSON: + def parse( + self, + file_path: Optional[Path] = None, + tmp_dir: Any = None, + ) -> JSON: """ Parse the matrix content """ diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/output_series_matrix.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/output_series_matrix.py index 67dd1b9f44..cdad8fb39e 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/output_series_matrix.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/output_series_matrix.py @@ -1,7 +1,7 @@ import logging -from typing import List, Optional, cast, Union +from pathlib import Path +from typing import List, Optional, cast, Union, Any -import numpy as np import pandas as pd # type: ignore from pandas import DataFrame @@ -56,9 +56,14 @@ def get_lazy_content( ) -> str: return f"matrixfile://{self.config.path.name}" - def parse_dataframe(self) -> DataFrame: + def parse_dataframe( + self, + file_path: Optional[Path] = None, + tmp_dir: Any = None, + ) -> DataFrame: + file_path = file_path or self.config.path df = pd.read_csv( - self.config.path, + file_path, sep="\t", skiprows=4, header=[0, 1, 2], @@ -66,6 +71,9 @@ def parse_dataframe(self) -> DataFrame: float_precision="legacy", ) + if tmp_dir: + tmp_dir.cleanup() + date, body = self.date_serializer.extract_date(df) matrix = rename_unnamed(body).astype(float) @@ -76,8 +84,10 @@ def parse_dataframe(self) -> DataFrame: def parse( self, + file_path: Optional[Path] = None, + tmp_dir: Any = None, ) -> JSON: - matrix = self.parse_dataframe() + matrix = self.parse_dataframe(file_path, tmp_dir) return cast(JSON, matrix.to_dict(orient="split")) def _dump_json(self, data: JSON) -> None: @@ -124,14 +134,19 @@ def load( expanded: bool = False, formatted: bool = True, ) -> Union[bytes, JSON]: + file_path, tmp_dir = self._get_real_file_path() if not formatted: - if self.config.path.exists(): - return self.config.path.read_bytes() + if file_path.exists(): + if tmp_dir: + tmp_dir.cleanup() + return file_path.read_bytes() logger.warning(f"Missing file {self.config.path}") + if tmp_dir: + tmp_dir.cleanup() return b"" - return self.parse() + return self.parse(file_path, tmp_dir) def dump( self, data: Union[bytes, JSON], url: Optional[List[str]] = None diff --git a/antarest/study/storage/rawstudy/model/filesystem/raw_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/raw_file_node.py index 111ff3bc76..fe9c4b90d9 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/raw_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/raw_file_node.py @@ -37,11 +37,19 @@ def load( expanded: bool = False, formatted: bool = True, ) -> bytes: - if self.config.path.exists(): - return self.config.path.read_bytes() - logger.warning(f"Missing file {self.config.path}") - return b"" + file_path, tmp_dir = self._get_real_file_path() + + if file_path.exists(): + bytes = file_path.read_bytes() + else: + logger.warning(f"Missing file {self.config.path}") + bytes = b"" + + if tmp_dir: + tmp_dir.cleanup() + + return bytes def dump(self, data: bytes, url: Optional[List[str]] = None) -> None: self.config.path.parent.mkdir(exist_ok=True, parents=True) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/areas/list.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/areas/list.py index 9b17b5b826..c6e22df06e 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/areas/list.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/areas/list.py @@ -8,7 +8,6 @@ ) from antarest.study.storage.rawstudy.model.filesystem.inode import ( INode, - TREE, ) @@ -39,10 +38,16 @@ def get( expanded: bool = False, formatted: bool = True, ) -> List[str]: - lines = self.config.path.read_text().split("\n") + if self.config.zip_path: + path, tmp_dir = self._extract_file_to_tmp_dir() + lines = path.read_text().split("\n") + tmp_dir.cleanup() + else: + lines = self.config.path.read_text().split("\n") return [l.strip() for l in lines if l.strip()] def save(self, data: List[str], url: Optional[List[str]] = None) -> None: + self._assert_not_in_zipped_file() self.config.path.write_text("\n".join(data)) def delete(self, url: Optional[List[str]] = None) -> None: diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/output/output.py b/antarest/study/storage/rawstudy/model/filesystem/root/output/output.py index 601e973616..84a25f1a1d 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/output/output.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/output/output.py @@ -1,6 +1,3 @@ -from antarest.study.storage.rawstudy.model.filesystem.config.model import ( - FileStudyTreeConfig, -) from antarest.study.storage.rawstudy.model.filesystem.folder_node import ( FolderNode, ) @@ -14,7 +11,9 @@ class Output(FolderNode): def build(self) -> TREE: children: TREE = { str(s.get_file()): OutputSimulation( - self.context, self.config.next_file(s.get_file()), s + self.context, + self.config.next_file(s.get_file(), is_output=True), + s, ) for i, s in self.config.outputs.items() } diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/ts_numbers/ts_numbers_data.py b/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/ts_numbers/ts_numbers_data.py index caa7a58b79..631c8913c6 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/ts_numbers/ts_numbers_data.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/ts_numbers/ts_numbers_data.py @@ -15,12 +15,20 @@ def load( expanded: bool = False, formatted: bool = True, ) -> List[int]: - if self.config.path.exists(): - with open(self.config.path, "r") as fh: + file_path, tmp_dir = self._get_real_file_path() + + if file_path.exists(): + with open(file_path, "r") as fh: data = fh.readlines() - if len(data) >= 1: - return [int(d) for d in data[1:]] + if tmp_dir: + tmp_dir.cleanup() + + if len(data) >= 1: + return [int(d) for d in data[1:]] + + if tmp_dir: + tmp_dir.cleanup() logger.warning(f"Missing file {self.config.path}") return [] diff --git a/antarest/study/storage/rawstudy/model/helpers.py b/antarest/study/storage/rawstudy/model/helpers.py index 8bb25de6b9..18507bfd19 100644 --- a/antarest/study/storage/rawstudy/model/helpers.py +++ b/antarest/study/storage/rawstudy/model/helpers.py @@ -1,56 +1,26 @@ -import tempfile -from pathlib import Path from typing import Optional, List, cast -from zipfile import ZipFile -from antarest.core.exceptions import ShouldNotHappenException from antarest.core.model import JSON from antarest.core.utils.utils import assert_this -from antarest.study.storage.rawstudy.io.reader import MultipleSameKeysIniReader from antarest.study.storage.rawstudy.model.filesystem.config.files import ( ConfigPathBuilder, ) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import ( - DUPLICATE_KEYS, -) class FileStudyHelpers: @staticmethod def get_config(study: FileStudy, output_id: Optional[str] = None) -> JSON: - config_path = ["settings", "generaldata"] if output_id: - if study.config.outputs[output_id].archived: - # TODO: remove this part of code when study tree zipfile support is implemented - if study.config.output_path is None: - raise ShouldNotHappenException() - output_path = study.config.output_path / f"{output_id}.zip" - tmp_dir = tempfile.TemporaryDirectory() - with ZipFile(output_path, "r") as zip_obj: - zip_obj.extract( - "about-the-study/parameters.ini", - tmp_dir.name, - ) - full_path_parameters = ( - Path(tmp_dir.name) - / "about-the-study" - / "parameters.ini" - ) - config = MultipleSameKeysIniReader(DUPLICATE_KEYS).read( - full_path_parameters - ) - tmp_dir.cleanup() - else: - config_path = [ - "output", - output_id, - "about-the-study", - "parameters", - ] - config = study.tree.get(config_path) + config_path = [ + "output", + output_id, + "about-the-study", + "parameters", + ] + config = study.tree.get(config_path) return config - return study.tree.get(config_path) + return study.tree.get(["settings", "generaldata"]) @staticmethod def save_config(study: FileStudy, config: JSON) -> None: diff --git a/antarest/study/storage/rawstudy/raw_study_service.py b/antarest/study/storage/rawstudy/raw_study_service.py index c0367c08c9..e850aeb002 100644 --- a/antarest/study/storage/rawstudy/raw_study_service.py +++ b/antarest/study/storage/rawstudy/raw_study_service.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import Optional, IO, List from uuid import uuid4 +from zipfile import ZipFile from antarest.core.config import Config from antarest.core.exceptions import ( @@ -119,16 +120,23 @@ def update_from_raw_meta( else: raise e - def exists(self, metadata: RawStudy) -> bool: + def exists(self, study: RawStudy) -> bool: """ Check study exist. Args: - metadata: study + study: study Returns: true if study presents in disk, false else. """ - return (self.get_study_path(metadata) / "study.antares").is_file() + path = self.get_study_path(study) + + if study.archived: + path = self.get_archive_path(study) + zf = ZipFile(path, "r") + return str("study.antares") in zf.namelist() + + return (path / "study.antares").is_file() def get_raw( self, @@ -285,7 +293,7 @@ def import_study(self, metadata: RawStudy, stream: IO[bytes]) -> Study: Returns: new study information. """ - path_study = self.get_study_path(metadata) + path_study = Path(metadata.path) path_study.mkdir() try: @@ -349,10 +357,13 @@ def set_reference_output( self.patch_service.set_reference_output(study, output_id, status) remove_from_cache(self.cache, study.id) - def archive(self, study: RawStudy) -> None: + def archive(self, study: RawStudy) -> Path: archive_path = self.get_archive_path(study) - self.export_study(study, archive_path) + new_study_path = self.export_study(study, archive_path) shutil.rmtree(study.path) + remove_from_cache(cache=self.cache, root_id=study.id) + self.cache.invalidate(study.id) + return new_study_path def get_archive_path(self, study: RawStudy) -> Path: return Path(self.config.storage.archive_dir / f"{study.id}.zip") @@ -366,6 +377,8 @@ def get_study_path(self, metadata: Study) -> Path: Returns: study path """ + if metadata.archived: + return self.get_archive_path(metadata) return Path(metadata.path) def initialize_additional_data(self, raw_study: RawStudy) -> bool: diff --git a/antarest/study/storage/rawstudy/watcher.py b/antarest/study/storage/rawstudy/watcher.py index 0648b0fd15..ed4aaa7ad2 100644 --- a/antarest/study/storage/rawstudy/watcher.py +++ b/antarest/study/storage/rawstudy/watcher.py @@ -129,16 +129,18 @@ def _rec_scan( f"No scan directive file found. Will skip further scan of folder {path}" ) return [] + if (path / "study.antares").exists(): logger.debug(f"Study {path.name} found in {workspace}") return [StudyFolder(path, workspace, groups)] + else: folders: List[StudyFolder] = list() if path.is_dir(): for child in path.iterdir(): try: if ( - child.is_dir() + (child.is_dir()) and any( [ re.search(regex, child.name) diff --git a/antarest/study/storage/utils.py b/antarest/study/storage/utils.py index a1a3299dfd..15df17c0c2 100644 --- a/antarest/study/storage/utils.py +++ b/antarest/study/storage/utils.py @@ -7,7 +7,7 @@ from math import ceil from pathlib import Path from time import strptime -from typing import Optional, Union, cast, List, Callable +from typing import Optional, Union, cast, Callable from uuid import uuid4 from zipfile import ZipFile diff --git a/examples/studies/STA-mini.zip b/examples/studies/STA-mini.zip index b23a787a25897643b7823c8c99202f82a31ccae8..c033602313c66c3398425c3ce90d39487a8065ad 100644 GIT binary patch delta 12732 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 delta 27114 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 JSON: return MOCK_MATRIX_JSON diff --git a/tests/storage/repository/filesystem/test_folder_node.py b/tests/storage/repository/filesystem/test_folder_node.py index 866d16aced..92e927ad84 100644 --- a/tests/storage/repository/filesystem/test_folder_node.py +++ b/tests/storage/repository/filesystem/test_folder_node.py @@ -25,6 +25,7 @@ def build_tree() -> INode: config = Mock() config.path.exist.return_value = True + config.zip_path = None return TestMiddleNode( context=Mock(), config=config, diff --git a/tests/storage/test_model.py b/tests/storage/test_model.py index 923352c7ad..ced31303b4 100644 --- a/tests/storage/test_model.py +++ b/tests/storage/test_model.py @@ -46,7 +46,7 @@ def test_file_study_tree_config_dto(): enr_modelling="aggregated", ) config_dto = FileStudyTreeConfigDTO.from_build_config(config) - assert list(config_dto.dict().keys()) + ["cache"] == list( - config.__dict__.keys() + assert sorted(list(config_dto.dict().keys()) + ["cache"]) == sorted( + list(config.__dict__.keys()) ) assert config_dto.to_build_config() == config diff --git a/webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx index f1fee86ec3..1b9f366980 100644 --- a/webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx @@ -8,7 +8,10 @@ import CreateVariantModal from "./CreateVariantModal"; import LauncherHistory from "./LauncherHistory"; import Notes from "./Notes"; import LauncherDialog from "../../../Studies/LauncherDialog"; -import { copyStudy } from "../../../../../services/api/study"; +import { + copyStudy, + unarchiveStudy as callUnarchiveStudy, +} from "../../../../../services/api/study"; import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; interface Props { @@ -37,6 +40,17 @@ function InformationView(props: Props) { } }; + const unarchiveStudy = async (study: StudyMetadata) => { + try { + await callUnarchiveStudy(study.id); + } catch (e) { + enqueueErrorSnackbar( + t("studies.error.unarchive", { studyname: study.name }), + e as AxiosError + ); + } + }; + return ( {t("global.open")} - {study && ( + {study && !study.archived && (
{study && tree && openVariantModal && ( diff --git a/webapp/src/components/App/Singlestudy/NavHeader.tsx b/webapp/src/components/App/Singlestudy/NavHeader.tsx index 62f8a3a270..0b1f4ed399 100644 --- a/webapp/src/components/App/Singlestudy/NavHeader.tsx +++ b/webapp/src/components/App/Singlestudy/NavHeader.tsx @@ -296,14 +296,16 @@ function NavHeader(props: Props) { alignItems="center" boxSizing="border-box" > - {isExplorer === true && ( + {isExplorer && ( )} {study && study.type === "variantstudy" && ( diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx index d07dc91403..987ffd151e 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx @@ -44,7 +44,7 @@ function DebugView() { ); useEffect(() => { - if (study && !study.archived) { + if (study) { initStudyData(study.id); } }, [study, initStudyData]); diff --git a/webapp/src/components/App/Studies/StudyCard.tsx b/webapp/src/components/App/Studies/StudyCard.tsx index 5704341f74..96d0653615 100644 --- a/webapp/src/components/App/Studies/StudyCard.tsx +++ b/webapp/src/components/App/Studies/StudyCard.tsx @@ -369,20 +369,12 @@ const StudyCard = memo((props: Props) => {
- {study.archived ? ( - - ) : ( - - - - )} + + - + ) : ( (); - - //////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////// - - const getCurrentConfig = () => { - return getValues("thematicTrimmingConfig"); - }; - - //////////////////////////////////////////////////////////////// - // Event Handlers - //////////////////////////////////////////////////////////////// - - const handleUpdateConfig = (fn: Pred) => () => { - const config = getCurrentConfig(); - const fieldNames = getFieldNames(study.version); - setValue("thematicTrimmingConfig", { - ...getCurrentConfig(), - ...R.map(fn, R.pick(fieldNames, config)), - }); - }; - - //////////////////////////////////////////////////////////////// - // JSX - //////////////////////////////////////////////////////////////// - - register("thematicTrimmingConfig", { - onAutoSubmit: () => { - const config = getCurrentConfig(); - const configDTO = thematicTrimmingConfigToDTO(config); - return setThematicTrimmingConfig(study.id, configDTO); - }, - }); - - return ( - {t("button.close")}} - contentProps={{ - sx: { pb: 0 }, - }} - > - - - - - - - - {getColumns(study.version).map((column, index) => ( - - {column.map(([label, name]) => ( - - ))} - - ))} - - - ); -} - -export default ThematicTrimmingDialog; diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/General/dialogs/ThematicTrimmingDialog/utils.ts b/webapp/src/components/App/Singlestudy/explore/Configuration/General/dialogs/ThematicTrimmingDialog/utils.ts deleted file mode 100644 index 81a3833de4..0000000000 --- a/webapp/src/components/App/Singlestudy/explore/Configuration/General/dialogs/ThematicTrimmingDialog/utils.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { camelCase } from "lodash"; -import * as R from "ramda"; -import * as RA from "ramda-adjunct"; -import { - StudyMetadata, - ThematicTrimmingConfigDTO, -} from "../../../../../../../../common/types"; - -export interface ThematicTrimmingConfig { - ovCost: boolean; - opCost: boolean; - mrgPrice: boolean; - co2Emis: boolean; - dtgByPlant: boolean; - balance: boolean; - rowBal: boolean; - psp: boolean; - miscNdg: boolean; - load: boolean; - hRor: boolean; - wind: boolean; - solar: boolean; - nuclear: boolean; - lignite: boolean; - coal: boolean; - gas: boolean; - oil: boolean; - mixFuel: boolean; - miscDtg: boolean; - hStor: boolean; - hPump: boolean; - hLev: boolean; - hInfl: boolean; - hOvfl: boolean; - hVal: boolean; - hCost: boolean; - unspEnrg: boolean; - spilEnrg: boolean; - lold: boolean; - lolp: boolean; - avlDtg: boolean; - dtgMrg: boolean; - maxMrg: boolean; - npCost: boolean; - npCostByPlant: boolean; - nodu: boolean; - noduByPlant: boolean; - flowLin: boolean; - ucapLin: boolean; - loopFlow: boolean; - flowQuad: boolean; - congFeeAlg: boolean; - congFeeAbs: boolean; - margCost: boolean; - congProdPlus: boolean; - congProdMinus: boolean; - hurdleCost: boolean; - // Study version >= 810 - resGenerationByPlant?: boolean; - miscDtg2?: boolean; - miscDtg3?: boolean; - miscDtg4?: boolean; - windOffshore?: boolean; - windOnshore?: boolean; - solarConcrt?: boolean; - solarPv?: boolean; - solarRooft?: boolean; - renw1?: boolean; - renw2?: boolean; - renw3?: boolean; - renw4?: boolean; -} - -const keysMap = { - ovCost: "OV. COST", - opCost: "OP. COST", - mrgPrice: "MRG. PRICE", - co2Emis: "CO2 EMIS.", - dtgByPlant: "DTG by plant", - balance: "BALANCE", - rowBal: "ROW BAL.", - psp: "PSP", - miscNdg: "MISC. NDG", - load: "LOAD", - hRor: "H. ROR", - wind: "WIND", - solar: "SOLAR", - nuclear: "NUCLEAR", - lignite: "LIGNITE", - coal: "COAL", - gas: "GAS", - oil: "OIL", - mixFuel: "MIX. FUEL", - miscDtg: "MISC. DTG", - hStor: "H. STOR", - hPump: "H. PUMP", - hLev: "H. LEV", - hInfl: "H. INFL", - hOvfl: "H. OVFL", - hVal: "H. VAL", - hCost: "H. COST", - unspEnrg: "UNSP. ENRG", - spilEnrg: "SPIL. ENRG", - lold: "LOLD", - lolp: "LOLP", - avlDtg: "AVL DTG", - dtgMrg: "DTG MRG", - maxMrg: "MAX MRG", - npCost: "NP COST", - npCostByPlant: "NP Cost by plant", - nodu: "NODU", - noduByPlant: "NODU by plant", - flowLin: "FLOW LIN.", - ucapLin: "UCAP LIN.", - loopFlow: "LOOP FLOW", - flowQuad: "FLOW QUAD.", - congFeeAlg: "CONG. FEE (ALG.)", - congFeeAbs: "CONG. FEE (ABS.)", - margCost: "MARG. COST", - congProdPlus: "CONG. PROD +", - congProdMinus: "CONG. PROD -", - hurdleCost: "HURDLE COST", - resGenerationByPlant: "RES generation by plant", - miscDtg2: "MISC. DTG 2", - miscDtg3: "MISC. DTG 3", - miscDtg4: "MISC. DTG 4", - windOffshore: "WIND OFFSHORE", - windOnshore: "WIND ONSHORE", - solarConcrt: "SOLAR CONCRT.", - solarPv: "SOLAR PV", - solarRooft: "SOLAR ROOFT", - renw1: "RENW. 1", - renw2: "RENW. 2", - renw3: "RENW. 3", - renw4: "RENW. 4", -}; - -export function formatThematicTrimmingConfigDTO( - configDTO: ThematicTrimmingConfigDTO -): ThematicTrimmingConfig { - return Object.entries(configDTO).reduce((acc, [key, value]) => { - const newKey = R.cond([ - [R.equals("CONG. PROD +"), R.always("congProdPlus")], - [R.equals("CONG. PROD -"), R.always("congProdMinus")], - [R.T, camelCase], - ])(key) as keyof ThematicTrimmingConfig; - - acc[newKey] = value; - return acc; - }, {} as ThematicTrimmingConfig); -} - -export function thematicTrimmingConfigToDTO( - config: ThematicTrimmingConfig -): ThematicTrimmingConfigDTO { - return RA.renameKeys(keysMap, config) as ThematicTrimmingConfigDTO; -} - -export function getColumns( - studyVersion: StudyMetadata["version"] -): Array> { - const version = Number(studyVersion); - - return [ - [ - ["OV. Cost", "ovCost"], - ["CO2 Emis.", "co2Emis"], - ["Balance", "balance"], - ["MISC. NDG", "miscNdg"], - ["Wind", "wind"], - ["Lignite", "lignite"], - ], - [ - ["OP. Cost", "opCost"], - ["DTG by plant", "dtgByPlant"], - ["Row bal.", "rowBal"], - ["Load", "load"], - ["Solar", "solar"], - ], - [ - ["MRG. Price", "mrgPrice"], - version >= 810 && ["RES generation by plant", "resGenerationByPlant"], - ["PSP", "psp"], - ["H. ROR", "hRor"], - ["Nuclear", "nuclear"], - ].filter(Boolean) as Array<[string, keyof ThematicTrimmingConfig]>, - ]; -} - -export function getFieldNames( - studyVersion: StudyMetadata["version"] -): Array { - return getColumns(studyVersion).flatMap((column) => { - return column.map(([, fieldName]) => fieldName); - }); -} diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx index 34b42a94be..504ba05a26 100644 --- a/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/index.tsx @@ -1,6 +1,5 @@ import { useOutletContext } from "react-router"; import * as R from "ramda"; -import { useState } from "react"; import { StudyMetadata } from "../../../../../../common/types"; import usePromiseWithSnackbarError from "../../../../../../hooks/usePromiseWithSnackbarError"; import { getFormValues } from "./utils"; @@ -8,29 +7,15 @@ import { PromiseStatus } from "../../../../../../hooks/usePromise"; import Form from "../../../../../common/Form"; import Fields from "./Fields"; import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; -import ThematicTrimmingDialog from "./dialogs/ThematicTrimmingDialog"; function GeneralParameters() { const { study } = useOutletContext<{ study: StudyMetadata }>(); - const [dialog, setDialog] = useState<"thematicTrimming" | "">(""); const { data, status, error } = usePromiseWithSnackbarError( () => getFormValues(study.id), { errorMessage: "Cannot get study data", deps: [study.id] } // TODO i18n ); - //////////////////////////////////////////////////////////////// - // Event Handlers - //////////////////////////////////////////////////////////////// - - const handleCloseDialog = () => { - setDialog(""); - }; - - //////////////////////////////////////////////////////////////// - // JSX - //////////////////////////////////////////////////////////////// - return R.cond([ [ R.either(R.equals(PromiseStatus.Idle), R.equals(PromiseStatus.Pending)), @@ -41,19 +26,7 @@ function GeneralParameters() { R.equals(PromiseStatus.Resolved), () => (
- - {R.cond([ - [ - R.equals("thematicTrimming"), - () => ( - - ), - ], - ])(dialog)} + ), ], diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts b/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts index 71ad0048ee..e99b91e072 100644 --- a/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts @@ -1,13 +1,6 @@ import * as RA from "ramda-adjunct"; import { StudyMetadata } from "../../../../../../common/types"; -import { - getStudyData, - getThematicTrimmingConfig, -} from "../../../../../../services/api/study"; -import { - ThematicTrimmingConfig, - formatThematicTrimmingConfigDTO, -} from "./dialogs/ThematicTrimmingDialog/utils"; +import { getStudyData } from "../../../../../../services/api/study"; enum Month { January = "january", @@ -122,11 +115,10 @@ export interface FormValues { mcScenario: SettingsGeneralDataOutput["storenewset"]; geographicTrimming: SettingsGeneralDataGeneral["geographic-trimming"]; thematicTrimming: SettingsGeneralDataGeneral["thematic-trimming"]; - thematicTrimmingConfig: ThematicTrimmingConfig; filtering: SettingsGeneralDataGeneral["filtering"]; } -const DEFAULT_VALUES: Omit = { +const DEFAULT_VALUES: FormValues = { mode: "Adequacy", firstDay: 1, lastDay: 1, @@ -170,8 +162,6 @@ export async function getFormValues( buildingMode = "Custom"; } - const thematicTrimmingConfigDto = await getThematicTrimmingConfig(studyId); - return { ...DEFAULT_VALUES, ...RA.renameKeys( @@ -198,8 +188,5 @@ export async function getFormValues( output ), buildingMode, - thematicTrimmingConfig: formatThematicTrimmingConfigDTO( - thematicTrimmingConfigDto - ), }; } diff --git a/webapp/src/components/common/Form/index.tsx b/webapp/src/components/common/Form/index.tsx index ad905534f7..098345cc12 100644 --- a/webapp/src/components/common/Form/index.tsx +++ b/webapp/src/components/common/Form/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { FormEvent, useCallback, useEffect, useRef } from "react"; +import { FormEvent, useCallback, useRef } from "react"; import { FieldPath, FieldPathValue, @@ -26,7 +26,6 @@ import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; import BackdropLoading from "../loaders/BackdropLoading"; import useDebounce from "../../../hooks/useDebounce"; import { getDirtyValues, stringToPath, toAutoSubmitConfig } from "./utils"; -import useAutoUpdateRef from "../../../hooks/useAutoUpdateRef"; export interface SubmitHandlerData< TFieldValues extends FieldValues = FieldValues @@ -44,7 +43,7 @@ export interface UseFormRegisterReturnPlus< TFieldValues extends FieldValues = FieldValues, TFieldName extends FieldPath = FieldPath > extends UseFormRegisterReturn { - value?: FieldPathValue; + defaultValue?: FieldPathValue; error?: boolean; helperText?: string; } @@ -116,7 +115,8 @@ function Form( mode: "onChange", ...config, }); - const { handleSubmit, formState, reset, watch } = formObj; + const { handleSubmit, formState, register, unregister, reset, setValue } = + formObj; const { isValid, isSubmitting, isDirty, dirtyFields, errors } = formState; const allowSubmit = isDirty && isValid && !isSubmitting; const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); @@ -127,16 +127,6 @@ function Form( Record any | Promise) | undefined> >({}); const lastDataSubmitted = useRef>(); - const preventClose = useRef(false); - const watchAllFields = watch(); - const wrapperFnsData = useAutoUpdateRef({ - fieldValues: watchAllFields, - fieldErrors: errors, - register: formObj.register, - unregister: formObj.unregister, - setValue: formObj.setValue, - isAutoConfigEnabled: autoSubmitConfig.enable, - }); useUpdateEffect( () => { @@ -151,21 +141,6 @@ function Form( [formState] ); - useEffect(() => { - const listener = (event: BeforeUnloadEvent) => { - if (preventClose.current) { - // eslint-disable-next-line no-param-reassign - event.returnValue = "Form not submitted yet. Sure you want to leave?"; // TODO i18n - } - }; - - window.addEventListener("beforeunload", listener); - - return () => { - window.removeEventListener("beforeunload", listener); - }; - }, []); - //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// @@ -199,37 +174,21 @@ function Form( } return Promise.all(res); - })() - .catch((error) => { - enqueueErrorSnackbar(t("form.submit.error"), error); - }) - .finally(() => { - preventClose.current = false; - }); + })().catch((error) => { + enqueueErrorSnackbar(t("form.submit.error"), error); + }); }; //////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////// - const simulateSubmitClick = useDebounce(() => { + const simulateSubmit = useDebounce(() => { submitRef.current?.click(); }, autoSubmitConfig.wait); - const simulateSubmit = useAutoUpdateRef(() => { - preventClose.current = true; - simulateSubmitClick(); - }); - - //////////////////////////////////////////////////////////////// - // API - //////////////////////////////////////////////////////////////// - const registerWrapper = useCallback>( (name, options) => { - const { register, fieldValues, fieldErrors, isAutoConfigEnabled } = - wrapperFnsData.current; - if (options?.onAutoSubmit) { fieldAutoSubmitListeners.current[name] = options.onAutoSubmit; } @@ -238,8 +197,8 @@ function Form( ...options, onChange: (e: unknown) => { options?.onChange?.(e); - if (isAutoConfigEnabled) { - simulateSubmit.current(); + if (autoSubmitConfig.enable) { + simulateSubmit(); } }, }; @@ -249,9 +208,11 @@ function Form( typeof name >; - const error = fieldErrors[name]; + const error = errors[name]; - res.value = R.path(name.split("."), fieldValues); + if (RA.isNotNil(config?.defaultValues?.[name])) { + res.defaultValue = config?.defaultValues?.[name]; + } if (error) { res.error = true; @@ -262,14 +223,17 @@ function Form( return res; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [ + autoSubmitConfig.enable, + config?.defaultValues, + errors, + register, + simulateSubmit, + ] ); const unregisterWrapper = useCallback>( (name, options) => { - const { unregister } = wrapperFnsData.current; - if (name) { const names = RA.ensureArray(name) as Path[]; names.forEach((n) => { @@ -278,27 +242,23 @@ function Form( } return unregister(name, options); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [unregister] ); const setValueWrapper = useCallback>( (name, value, options) => { - const { setValue, isAutoConfigEnabled } = wrapperFnsData.current; - const newOptions: typeof options = { - shouldDirty: isAutoConfigEnabled, // Option false by default + shouldDirty: autoSubmitConfig.enable, // Option false by default ...options, }; - if (isAutoConfigEnabled && newOptions.shouldDirty) { - simulateSubmit.current(); + if (autoSubmitConfig.enable && newOptions.shouldDirty) { + simulateSubmit(); } setValue(name, value, newOptions); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [autoSubmitConfig.enable, setValue, simulateSubmit] ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/common/fieldEditors/SelectFE.tsx b/webapp/src/components/common/fieldEditors/SelectFE.tsx index d72d586e3b..2728a72b70 100644 --- a/webapp/src/components/common/fieldEditors/SelectFE.tsx +++ b/webapp/src/components/common/fieldEditors/SelectFE.tsx @@ -56,7 +56,12 @@ const SelectFE = forwardRef((props: SelectFEProps, ref) => { return ( {label} - {emptyValue && ( {/* TODO i18n */} diff --git a/webapp/src/components/common/fieldEditors/SwitchFE.tsx b/webapp/src/components/common/fieldEditors/SwitchFE.tsx index a990903916..d7c5b3e33b 100644 --- a/webapp/src/components/common/fieldEditors/SwitchFE.tsx +++ b/webapp/src/components/common/fieldEditors/SwitchFE.tsx @@ -4,7 +4,6 @@ import { Switch, SwitchProps, } from "@mui/material"; -import clsx from "clsx"; import { forwardRef } from "react"; export interface SwitchFEProps @@ -28,7 +27,6 @@ const SwitchFE = forwardRef((props: SwitchFEProps, ref) => { labelPlacement, helperText, error, - className, sx, ...rest } = props; @@ -36,7 +34,6 @@ const SwitchFE = forwardRef((props: SwitchFEProps, ref) => { const fieldEditor = ( { return ( (value: T): React.MutableRefObject { - const ref = useRef(value); - - useEffect(() => { - ref.current = value; - }); - - return ref; -} - -export default useAutoUpdateRef; diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 7d75e27646..821129b1ca 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -13,7 +13,6 @@ import { AreasConfig, LaunchJobDTO, StudyMetadataPatchDTO, - ThematicTrimmingConfigDTO, } from "../../common/types"; import { getConfig } from "../config"; import { convertStudyDtoToMetadata } from "../utils"; @@ -388,18 +387,4 @@ export const scanFolder = async (folderPath: string): Promise => { await client.post(`/v1/watcher/_scan?path=${encodeURIComponent(folderPath)}`); }; -export const getThematicTrimmingConfig = async ( - studyId: StudyMetadata["id"] -): Promise => { - const res = await client.get( - `/v1/studies/${studyId}/config/thematic_trimming` - ); - return res.data; -}; - -export const setThematicTrimmingConfig = async ( - studyId: StudyMetadata["id"], - config: ThematicTrimmingConfigDTO -): Promise => { - await client.put(`/v1/studies/${studyId}/config/thematic_trimming`, config); -}; +export default {}; diff --git a/webapp/src/theme.ts b/webapp/src/theme.ts index bfb2d709b5..0cb4c1dce9 100644 --- a/webapp/src/theme.ts +++ b/webapp/src/theme.ts @@ -11,6 +11,7 @@ export const STUDIES_FILTER_WIDTH = 300; const secondaryMainColor = "#00B2FF"; export const PAPER_BACKGROUND_NO_TRANSPARENCY = "#212c38"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const scrollbarStyle = { "&::-webkit-scrollbar": { width: "7px", @@ -129,6 +130,7 @@ const theme = createTheme({ background: "rgba(255, 255, 255, 0.09)", borderRadius: "4px 4px 0px 0px", borderBottom: "1px solid rgba(255, 255, 255, 0.42)", + paddingRight: 6, ".MuiSelect-icon": { backgroundColor: "#222333", },