diff --git a/antarest/__init__.py b/antarest/__init__.py index 823a3a566a..f8b3bd0418 100644 --- a/antarest/__init__.py +++ b/antarest/__init__.py @@ -19,9 +19,9 @@ # Standard project metadata -__version__ = "2.17.6" +__version__ = "2.18.0" __author__ = "RTE, Antares Web Team" -__date__ = "2024-09-25" +__date__ = "2024-11-29" # noinspection SpellCheckingInspection __credits__ = "(c) Réseau de Transport de l’Électricité (RTE)" diff --git a/antarest/core/exceptions.py b/antarest/core/exceptions.py index 5d574421a6..d41d04d9cf 100644 --- a/antarest/core/exceptions.py +++ b/antarest/core/exceptions.py @@ -300,6 +300,11 @@ def __init__(self, message: str) -> None: super().__init__(HTTPStatus.UNPROCESSABLE_ENTITY, message) +class LinkNotFound(HTTPException): + def __init__(self, message: str) -> None: + super().__init__(HTTPStatus.NOT_FOUND, message) + + class VariantStudyParentNotValid(HTTPException): def __init__(self, message: str) -> None: super().__init__(HTTPStatus.UNPROCESSABLE_ENTITY, message) @@ -348,13 +353,23 @@ def __init__(self, is_variant: bool) -> None: super().__init__(HTTPStatus.EXPECTATION_FAILED, "Upgrade not supported for parent of variants") -class FileDeletionNotAllowed(HTTPException): +class ResourceDeletionNotAllowed(HTTPException): """ Exception raised when deleting a file or a folder which isn't inside the 'User' folder. """ def __init__(self, message: str) -> None: - msg = f"Raw deletion failed because {message}" + msg = f"Resource deletion failed because {message}" + super().__init__(HTTPStatus.FORBIDDEN, msg) + + +class FolderCreationNotAllowed(HTTPException): + """ + Exception raised when creating a folder which isn't inside the 'User' folder. + """ + + def __init__(self, message: str) -> None: + msg = f"Folder creation failed because {message}" super().__init__(HTTPStatus.FORBIDDEN, msg) diff --git a/antarest/desktop/systray_app.py b/antarest/desktop/systray_app.py index b1d2a711fe..9d53ebb185 100644 --- a/antarest/desktop/systray_app.py +++ b/antarest/desktop/systray_app.py @@ -50,8 +50,13 @@ def start_server(config_file: Path) -> Process: return server -def open_app() -> None: - webbrowser.open("http://localhost:8080") +def open_app(wait_seconds: int = 0) -> None: + """ + Open antares-web in a new browser tab. + Optionally, waits for some seconds to ensure it does have time for opening. + """ + webbrowser.open_new_tab("http://localhost:8080") + time.sleep(wait_seconds) def monitor_server_process(server: Process, app: QApplication) -> None: @@ -173,7 +178,9 @@ def run_systray_app(config_file: Path) -> None: notification_popup( "Antares Web Server already running, you can manage the application within the system tray.", threaded=False ) - open_app() + # On windows at least, if the current process closes too fast, + # the browser does not have time to open --> waiting an arbitrary 10s + open_app(wait_seconds=10) return notification_popup("Starting Antares Web Server...") systray_app = create_systray_app() @@ -182,5 +189,8 @@ def run_systray_app(config_file: Path) -> None: wait_for_server_start() notification_popup("Antares Web Server started, you can manage the application within the system tray.") open_app() - systray_app.app.exec_() - server.kill() + try: + systray_app.app.exec_() + finally: + # Kill server also on exception, in particular on keyboard interrupt + server.kill() diff --git a/antarest/study/business/link_management.py b/antarest/study/business/link_management.py index 0a303d49a7..777ce3cd9e 100644 --- a/antarest/study/business/link_management.py +++ b/antarest/study/business/link_management.py @@ -11,20 +11,23 @@ # This file is part of the Antares project. import typing as t +from typing import Any, Dict from antares.study.version import StudyVersion -from antarest.core.exceptions import ConfigFileNotFound +from antarest.core.exceptions import ConfigFileNotFound, LinkNotFound, LinkValidationError from antarest.core.model import JSON from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model -from antarest.study.business.model.link_model import LinkDTO, LinkInternal +from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO, LinkInternal from antarest.study.business.utils import execute_or_add_commands from antarest.study.model import RawStudy, Study from antarest.study.storage.rawstudy.model.filesystem.config.links import LinkProperties +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_link import CreateLink from antarest.study.storage.variantstudy.model.command.remove_link import RemoveLink from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command.update_link import UpdateLink _ALL_LINKS_PATH = "input/links" @@ -76,6 +79,46 @@ def create_link(self, study: Study, link_creation_dto: LinkDTO) -> LinkDTO: return link_creation_dto + def update_link(self, study: RawStudy, area_from: str, area_to: str, link_update_dto: LinkBaseDTO) -> LinkDTO: + link_dto = LinkDTO(area1=area_from, area2=area_to, **link_update_dto.model_dump()) + + link = link_dto.to_internal(StudyVersion.parse(study.version)) + file_study = self.storage_service.get_storage(study).get_raw(study) + + self.get_link_if_exists(file_study, link) + + command = UpdateLink( + area1=area_from, + area2=area_to, + parameters=link.model_dump( + include=link_update_dto.model_fields_set, exclude={"area1", "area2"}, exclude_none=True + ), + command_context=self.storage_service.variant_study_service.command_factory.command_context, + study_version=file_study.config.version, + ) + + execute_or_add_commands(study, file_study, [command], self.storage_service) + + updated_link = self.get_internal_link(study, link) + + return updated_link.to_dto() + + def get_link_if_exists(self, file_study: FileStudy, link: LinkInternal) -> dict[str, Any]: + try: + return file_study.tree.get(["input", "links", link.area1, "properties", link.area2]) + except KeyError: + raise LinkNotFound(f"The link {link.area1} -> {link.area2} is not present in the study") + + def get_internal_link(self, study: RawStudy, link: LinkInternal) -> LinkInternal: + file_study = self.storage_service.get_storage(study).get_raw(study) + + link_properties = self.get_link_if_exists(file_study, link) + + link_properties.update({"area1": link.area1, "area2": link.area2}) + updated_link = LinkInternal.model_validate(link_properties) + + return updated_link + def delete_link(self, study: RawStudy, area1_id: str, area2_id: str) -> None: file_study = self.storage_service.get_storage(study).get_raw(study) command = RemoveLink( diff --git a/antarest/study/business/model/link_model.py b/antarest/study/business/model/link_model.py index d43a9e6e0b..977ce69ee3 100644 --- a/antarest/study/business/model/link_model.py +++ b/antarest/study/business/model/link_model.py @@ -36,18 +36,7 @@ ] -class Area(AntaresBaseModel): - area1: str - area2: str - - @model_validator(mode="after") - def validate_areas(self) -> t.Self: - if self.area1 == self.area2: - raise LinkValidationError(f"Cannot create a link that goes from and to the same single area: {self.area1}") - return self - - -class LinkDTO(Area): +class LinkBaseDTO(AntaresBaseModel): model_config = ConfigDict(alias_generator=to_camel_case, populate_by_name=True, extra="forbid") hurdles_cost: bool = False @@ -61,10 +50,25 @@ class LinkDTO(Area): colorg: int = Field(default=DEFAULT_COLOR, ge=0, le=255) link_width: float = 1 link_style: LinkStyle = LinkStyle.PLAIN - filter_synthesis: t.Optional[comma_separated_enum_list] = FILTER_VALUES filter_year_by_year: t.Optional[comma_separated_enum_list] = FILTER_VALUES + +class Area(AntaresBaseModel): + area1: str + area2: str + + @model_validator(mode="after") + def validate_areas(self) -> t.Self: + if self.area1 == self.area2: + raise LinkValidationError(f"Cannot create a link that goes from and to the same single area: {self.area1}") + area_from, area_to = sorted([self.area1, self.area2]) + self.area1 = area_from + self.area2 = area_to + return self + + +class LinkDTO(Area, LinkBaseDTO): def to_internal(self, version: StudyVersion) -> "LinkInternal": if version < STUDY_VERSION_8_2 and {"filter_synthesis", "filter_year_by_year"} & self.model_fields_set: raise LinkValidationError("Cannot specify a filter value for study's version earlier than v8.2") diff --git a/antarest/study/business/table_mode_management.py b/antarest/study/business/table_mode_management.py index ded9133a46..5905c78faf 100644 --- a/antarest/study/business/table_mode_management.py +++ b/antarest/study/business/table_mode_management.py @@ -201,7 +201,11 @@ def update_table_data( elif table_type == TableModeType.LINK: links_map = {tuple(key.split(" / ")): LinkOutput(**values) for key, values in data.items()} updated_map = self._link_manager.update_links_props(study, links_map) # type: ignore - excludes = set() if int(study.version) >= STUDY_VERSION_8_2 else {"filter_synthesis", "filter_year_by_year"} + excludes = ( + set() + if StudyVersion.parse(study.version) >= STUDY_VERSION_8_2 + else {"filter_synthesis", "filter_year_by_year"} + ) data = { f"{area1_id} / {area2_id}": link.model_dump(by_alias=True, exclude=excludes) for (area1_id, area2_id), link in updated_map.items() diff --git a/antarest/study/business/xpansion_management.py b/antarest/study/business/xpansion_management.py index 02e1fc795c..c86dd5cab7 100644 --- a/antarest/study/business/xpansion_management.py +++ b/antarest/study/business/xpansion_management.py @@ -21,7 +21,7 @@ from fastapi import HTTPException, UploadFile from pydantic import Field, ValidationError, field_validator, model_validator -from antarest.core.exceptions import BadZipBinary, ChildNotFoundError +from antarest.core.exceptions import BadZipBinary, ChildNotFoundError, LinkNotFound from antarest.core.model import JSON from antarest.core.serialization import AntaresBaseModel from antarest.study.business.all_optional_meta import all_optional_model @@ -255,11 +255,6 @@ class XpansionCandidateDTO(AntaresBaseModel): ) -class LinkNotFound(HTTPException): - def __init__(self, message: str) -> None: - super().__init__(http.HTTPStatus.NOT_FOUND, message) - - class XpansionFileNotFoundError(HTTPException): def __init__(self, message: str) -> None: super().__init__(http.HTTPStatus.NOT_FOUND, message) diff --git a/antarest/study/service.py b/antarest/study/service.py index 2f0e713266..8811be0528 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -36,13 +36,14 @@ BadEditInstructionException, ChildNotFoundError, CommandApplicationError, - FileDeletionNotAllowed, + FolderCreationNotAllowed, IncorrectPathError, NotAManagedStudyException, OutputAlreadyArchived, OutputAlreadyUnarchived, OutputNotFound, ReferencedObjectDeletionNotAllowed, + ResourceDeletionNotAllowed, StudyDeletionNotAllowed, StudyNotFoundError, StudyTypeUnsupported, @@ -89,7 +90,7 @@ from antarest.study.business.general_management import GeneralManager from antarest.study.business.link_management import LinkManager from antarest.study.business.matrix_management import MatrixManager, MatrixManagerError -from antarest.study.business.model.link_model import LinkDTO +from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO from antarest.study.business.optimization_management import OptimizationManager from antarest.study.business.playlist_management import PlaylistManager from antarest.study.business.scenario_builder_management import ScenarioBuilderManager @@ -155,10 +156,19 @@ remove_from_cache, ) from antarest.study.storage.variantstudy.business.utils import transform_command_to_dto +from antarest.study.storage.variantstudy.model.command.create_user_resource import ( + CreateUserResource, + CreateUserResourceData, + ResourceType, +) from antarest.study.storage.variantstudy.model.command.generate_thermal_cluster_timeseries import ( GenerateThermalClusterTimeSeries, ) from antarest.study.storage.variantstudy.model.command.icommand import ICommand +from antarest.study.storage.variantstudy.model.command.remove_user_resource import ( + RemoveUserResource, + RemoveUserResourceData, +) from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix from antarest.study.storage.variantstudy.model.command.update_comments import UpdateComments from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig @@ -189,6 +199,20 @@ def get_disk_usage(path: t.Union[str, Path]) -> int: return total_size +def _get_path_inside_user_folder( + path: str, exception_class: t.Type[t.Union[FolderCreationNotAllowed, ResourceDeletionNotAllowed]] +) -> str: + """ + Retrieves the path inside the `user` folder for a given user path + + Raises exception_class if the path is not located inside the `user` folder + """ + url = [item for item in path.split("/") if item] + if len(url) < 2 or url[0] != "user": + raise exception_class(f"the given path isn't inside the 'User' folder: {path}") + return "/".join(url[1:]) + + class TaskProgressRecorder(ICommandListener): def __init__(self, notifier: ITaskNotifier) -> None: self.notifier = notifier @@ -226,7 +250,7 @@ def _generate_timeseries(self, notifier: ITaskNotifier) -> None: ) execute_or_add_commands(study, file_study, [command], self.storage_service, listener) - if isinstance(file_study, VariantStudy): + if isinstance(study, VariantStudy): # In this case we only added the command to the list. # It means the generation will really be executed in the next snapshot generation. # We don't want this, we want this task to generate the matrices no matter the study. @@ -236,8 +260,10 @@ def _generate_timeseries(self, notifier: ITaskNotifier) -> None: generation_task_id = variant_service.generate_task(study, True, False, listener) task_service.await_task(generation_task_id) result = task_service.status_task(generation_task_id, RequestParameters(DEFAULT_ADMIN_USER)) - if not result.result or not result.result.success: - raise ValueError(f"Failed to generate variant study {self._study_id}") + assert result.result is not None + if not result.result.success: + raise ValueError(result.result.message) + self.event_bus.push( Event( type=EventType.STUDY_EDITED, @@ -1578,7 +1604,7 @@ def _edit_study_using_command( data: SUB_JSON, *, create_missing: bool = False, - ) -> ICommand: + ) -> t.List[ICommand]: """ Replace data on disk with new, using variant commands. @@ -1593,57 +1619,39 @@ def _edit_study_using_command( """ study_service = self.storage_service.get_storage(study) file_study = study_service.get_raw(metadata=study) + version = file_study.config.version + commands: t.List[ICommand] = [] file_relpath = PurePosixPath(url.strip().strip("/")) file_path = study_service.get_study_path(study).joinpath(file_relpath) create_missing &= not file_path.exists() if create_missing: - # IMPORTANT: We prohibit deep file system changes in private directories. - # - File and directory creation is only possible for the "user" directory, - # because the "input" and "output" directories are managed by Antares. - # - We also prohibit writing files in the "user/expansion" folder which currently - # contains the Xpansion tool configuration. - # This configuration should be moved to the "input/expansion" directory in the future. - if file_relpath and file_relpath.parts[0] == "user" and file_relpath.parts[1] != "expansion": - # In the case of variants, we must write the file directly in the study's snapshot folder, - # because the "user" folder is not managed by the command mechanism. - file_path.parent.mkdir(parents=True, exist_ok=True) - file_path.touch() - - # A 404 Not Found error is raised if the file does not exist. - tree_node = file_study.tree.get_node(file_relpath.parts) # type: ignore - - study_version = file_study.config.version - command = self._create_edit_study_command(tree_node=tree_node, url=url, data=data, study_version=study_version) + context = self.storage_service.variant_study_service.command_factory.command_context + user_path = _get_path_inside_user_folder(str(file_relpath), FolderCreationNotAllowed) + args = {"path": user_path, "resource_type": ResourceType.FILE} + command_data = CreateUserResourceData.model_validate(args) + cmd_1 = CreateUserResource(data=command_data, command_context=context, study_version=version) + assert isinstance(data, bytes) + cmd_2 = UpdateRawFile( + target=url, + b64Data=base64.b64encode(data).decode("utf-8"), + command_context=context, + study_version=version, + ) + commands.extend([cmd_1, cmd_2]) + else: + # A 404 Not Found error is raised if the file does not exist. + tree_node = file_study.tree.get_node(file_relpath.parts) # type: ignore + commands.append(self._create_edit_study_command(tree_node, url, data, version)) if isinstance(study_service, RawStudyService): - res = command.apply(study_data=file_study) - if not is_managed(study): - tree_node.denormalize() - if not res.status: - raise CommandApplicationError(res.message) - - # noinspection SpellCheckingInspection url = "study/antares/lastsave" last_save_node = file_study.tree.get_node(url.split("/")) - cmd = self._create_edit_study_command( - tree_node=last_save_node, url=url, data=int(time.time()), study_version=study_version - ) - cmd.apply(file_study) + cmd = self._create_edit_study_command(last_save_node, url, int(time.time()), version) + commands.append(cmd) - self.storage_service.variant_study_service.invalidate_cache(study) - - elif isinstance(study_service, VariantStudyService): - study_service.append_command( - study_id=file_study.config.study_id, - command=command.to_dto(), - params=RequestParameters(user=DEFAULT_ADMIN_USER), - ) - - else: # pragma: no cover - raise TypeError(repr(type(study_service))) - - return command # for testing purpose + execute_or_add_commands(study, file_study, commands, self.storage_service) + return commands # for testing purpose def apply_commands( self, uuid: str, commands: t.List[CommandDTO], params: RequestParameters @@ -1909,6 +1917,27 @@ def create_link( ) return new_link + def update_link( + self, + uuid: str, + area_from: str, + area_to: str, + link_update_dto: LinkBaseDTO, + params: RequestParameters, + ) -> LinkDTO: + study = self.get_study(uuid) + assert_permission(params.user, study, StudyPermissionType.WRITE) + self._assert_study_unarchived(study) + updated_link = self.links_manager.update_link(study, area_from, area_to, link_update_dto) + self.event_bus.push( + Event( + type=EventType.STUDY_DATA_EDITED, + payload=study.to_json_summary(), + permissions=PermissionInfo.from_study(study), + ) + ) + return updated_link + def update_area( self, uuid: str, @@ -2716,7 +2745,7 @@ def asserts_no_thermal_in_binding_constraints( binding_ids = [bc.id for bc in ref_bcs] raise ReferencedObjectDeletionNotAllowed(cluster_id, binding_ids, object_type="Cluster") - def delete_file_or_folder(self, study_id: str, path: str, current_user: JWTUser) -> None: + def delete_user_file_or_folder(self, study_id: str, path: str, current_user: JWTUser) -> None: """ Deletes a file or a folder of the study. The data must be located inside the 'User' folder. @@ -2728,28 +2757,58 @@ def delete_file_or_folder(self, study_id: str, path: str, current_user: JWTUser) current_user: User that called the endpoint Raises: - FileDeletionNotAllowed: if the path does not comply with the above rules + ResourceDeletionNotAllowed: if the path does not comply with the above rules """ - study = self.get_study(study_id) - assert_permission(current_user, study, StudyPermissionType.WRITE) + cmd_data = RemoveUserResourceData(**{"path": _get_path_inside_user_folder(path, ResourceDeletionNotAllowed)}) + self._alter_user_folder(study_id, cmd_data, RemoveUserResource, ResourceDeletionNotAllowed, current_user) - url = [item for item in path.split("/") if item] - if len(url) < 2 or url[0] != "user": - raise FileDeletionNotAllowed(f"the targeted data isn't inside the 'User' folder: {path}") + def create_user_folder(self, study_id: str, path: str, current_user: JWTUser) -> None: + """ + Creates a folder inside the study. + The data must be located inside the 'User' folder. + Also, it can not be inside the 'expansion' folder. - study_tree = self.storage_service.raw_study_service.get_raw(study, True).tree - user_node = t.cast(User, study_tree.get_node(["user"])) - if url[1] in [file.filename for file in user_node.registered_files]: - raise FileDeletionNotAllowed(f"you are not allowed to delete this resource : {path}") + Args: + study_id: UUID of the concerned study + path: Path corresponding to the resource to be deleted + current_user: User that called the endpoint + Raises: + FolderCreationNotAllowed: if the path does not comply with the above rules + """ + args = { + "path": _get_path_inside_user_folder(path, FolderCreationNotAllowed), + "resource_type": ResourceType.FOLDER, + } + command_data = CreateUserResourceData.model_validate(args) + self._alter_user_folder(study_id, command_data, CreateUserResource, FolderCreationNotAllowed, current_user) + + def _alter_user_folder( + self, + study_id: str, + command_data: t.Union[CreateUserResourceData, RemoveUserResourceData], + command_class: t.Type[t.Union[CreateUserResource, RemoveUserResource]], + exception_class: t.Type[t.Union[FolderCreationNotAllowed, ResourceDeletionNotAllowed]], + current_user: JWTUser, + ) -> None: + study = self.get_study(study_id) + assert_permission(current_user, study, StudyPermissionType.WRITE) + + args = { + "data": command_data, + "study_version": StudyVersion.parse(study.version), + "command_context": self.storage_service.variant_study_service.command_factory.command_context, + } + command = command_class.model_validate(args) + file_study = self.storage_service.get_storage(study).get_raw(study, True) try: - user_node.delete(url[1:]) - except ChildNotFoundError as e: - raise FileDeletionNotAllowed("the given path doesn't exist") from e + execute_or_add_commands(study, file_study, [command], self.storage_service) + except CommandApplicationError as e: + raise exception_class(e.detail) from e # update cache cache_id = f"{CacheConstants.RAW_STUDY}/{study.id}" - updated_tree = study_tree.get() + updated_tree = file_study.tree.get() self.storage_service.get_storage(study).cache.put(cache_id, updated_tree) # type: ignore def get_digest_file(self, study_id: str, output_id: str, params: RequestParameters) -> DigestUI: diff --git a/antarest/study/storage/abstract_storage_service.py b/antarest/study/storage/abstract_storage_service.py index 782eb5516f..ccaa477673 100644 --- a/antarest/study/storage/abstract_storage_service.py +++ b/antarest/study/storage/abstract_storage_service.py @@ -314,7 +314,7 @@ def export_output(self, metadata: T, output_id: str, target: Path) -> None: logger.info(f"Exporting output {output_id} from study {metadata.id}") path_output = Path(metadata.path) / "output" / output_id - path_output_zip = Path(metadata.path) / "output" / f"{output_id}.{ArchiveFormat.ZIP}" + path_output_zip = Path(metadata.path) / "output" / f"{output_id}{ArchiveFormat.ZIP}" if path_output_zip.exists(): shutil.copyfile(path_output_zip, target) diff --git a/antarest/study/storage/rawstudy/model/filesystem/inode.py b/antarest/study/storage/rawstudy/model/filesystem/inode.py index f88903c729..4b1046162a 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/inode.py +++ b/antarest/study/storage/rawstudy/model/filesystem/inode.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar -from antarest.core.exceptions import ShouldNotHappenException, WritingInsideZippedFileException +from antarest.core.exceptions import WritingInsideZippedFileException from antarest.core.utils.archives import extract_file_to_tmp_dir from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig @@ -137,9 +137,7 @@ def _assert_url_end(self, url: Optional[List[str]] = None) -> None: if len(url) > 0: raise ValueError(f"url should be fully resolved when arrives on {self.__class__.__name__}") - def _extract_file_to_tmp_dir( - self, - ) -> Tuple[Path, Any]: + def _extract_file_to_tmp_dir(self, archived_path: Path) -> 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. @@ -148,13 +146,8 @@ def _extract_file_to_tmp_dir( The actual path of the extracted file the tmp_dir object which MUST be cleared after use of the file """ - if self.config.archive_path is None: - raise ShouldNotHappenException() - inside_archive_path = self.config.path.relative_to(self.config.archive_path.parent / self.config.study_id) - if self.config.archive_path: - return extract_file_to_tmp_dir(self.config.archive_path, inside_archive_path) - else: - raise ShouldNotHappenException() + inside_archive_path = self.config.path.relative_to(archived_path.with_suffix("")) + return extract_file_to_tmp_dir(archived_path, inside_archive_path) def _assert_not_in_zipped_file(self) -> None: """Prevents writing inside a zip file""" diff --git a/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py b/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py index 6520802d90..2662cde82b 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py @@ -48,7 +48,7 @@ def _get_real_file_path( ) -> t.Tuple[Path, t.Any]: tmp_dir = None if self.config.archive_path: - path, tmp_dir = self._extract_file_to_tmp_dir() + path, tmp_dir = self._extract_file_to_tmp_dir(self.config.archive_path) else: path = self.config.path return path, tmp_dir diff --git a/antarest/study/storage/utils.py b/antarest/study/storage/utils.py index ac4ba9acfb..1b63e4afbe 100644 --- a/antarest/study/storage/utils.py +++ b/antarest/study/storage/utils.py @@ -132,7 +132,9 @@ def find_single_output_path(all_output_path: Path) -> Path: def is_output_archived(path_output: Path) -> bool: # Returns True it the given path is archived or if adding a suffix to the path points to an existing path suffixes = [".zip"] - return path_output.suffix in suffixes or any(path_output.with_suffix(suffix).exists() for suffix in suffixes) + if path_output.suffixes and path_output.suffixes[-1] in suffixes: + return True + return any((path_output.parent / (path_output.name + suffix)).exists() for suffix in suffixes) def extract_output_name(path_output: Path, new_suffix_name: t.Optional[str] = None) -> str: diff --git a/antarest/study/storage/variantstudy/business/command_reverter.py b/antarest/study/storage/variantstudy/business/command_reverter.py index 43dff3f981..7b585f46b5 100644 --- a/antarest/study/storage/variantstudy/business/command_reverter.py +++ b/antarest/study/storage/variantstudy/business/command_reverter.py @@ -28,6 +28,7 @@ from antarest.study.storage.variantstudy.model.command.create_link import CreateLink from antarest.study.storage.variantstudy.model.command.create_renewables_cluster import CreateRenewablesCluster from antarest.study.storage.variantstudy.model.command.create_st_storage import CreateSTStorage +from antarest.study.storage.variantstudy.model.command.create_user_resource import CreateUserResource from antarest.study.storage.variantstudy.model.command.generate_thermal_cluster_timeseries import ( GenerateThermalClusterTimeSeries, ) @@ -39,6 +40,7 @@ from antarest.study.storage.variantstudy.model.command.remove_link import RemoveLink from antarest.study.storage.variantstudy.model.command.remove_renewables_cluster import RemoveRenewablesCluster from antarest.study.storage.variantstudy.model.command.remove_st_storage import RemoveSTStorage +from antarest.study.storage.variantstudy.model.command.remove_user_resource import RemoveUserResource from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix from antarest.study.storage.variantstudy.model.command.update_binding_constraint import UpdateBindingConstraint from antarest.study.storage.variantstudy.model.command.update_comments import UpdateComments @@ -99,6 +101,10 @@ def _revert_create_link(base_command: CreateLink, history: t.List["ICommand"], b ) ] + @staticmethod + def _revert_update_link(base_command: CreateLink, history: t.List["ICommand"], base: FileStudy) -> t.List[ICommand]: + raise NotImplementedError("The revert function for UpdateLink is not available") + @staticmethod def _revert_remove_link(base_command: RemoveLink, history: t.List["ICommand"], base: FileStudy) -> t.List[ICommand]: raise NotImplementedError("The revert function for RemoveLink is not available") @@ -351,6 +357,24 @@ def _revert_generate_thermal_cluster_timeseries( ) -> t.List[ICommand]: raise NotImplementedError("The revert function for GenerateThermalClusterTimeSeries is not available") + @staticmethod + def _revert_create_user_resource( + base_command: CreateUserResource, history: t.List["ICommand"], base: FileStudy + ) -> t.List[ICommand]: + return [ + RemoveUserResource( + data=base_command.data, + command_context=base_command.command_context, + study_version=base_command.study_version, + ) + ] + + @staticmethod + def _revert_remove_user_resource( + base_command: RemoveUserResource, history: t.List["ICommand"], base: FileStudy + ) -> t.List[ICommand]: + raise NotImplementedError("The revert function for RemoveUserResource is not available") + def revert( self, base_command: ICommand, diff --git a/antarest/study/storage/variantstudy/command_factory.py b/antarest/study/storage/variantstudy/command_factory.py index c22b2319f7..567567a263 100644 --- a/antarest/study/storage/variantstudy/command_factory.py +++ b/antarest/study/storage/variantstudy/command_factory.py @@ -27,6 +27,7 @@ from antarest.study.storage.variantstudy.model.command.create_link import CreateLink from antarest.study.storage.variantstudy.model.command.create_renewables_cluster import CreateRenewablesCluster from antarest.study.storage.variantstudy.model.command.create_st_storage import CreateSTStorage +from antarest.study.storage.variantstudy.model.command.create_user_resource import CreateUserResource from antarest.study.storage.variantstudy.model.command.generate_thermal_cluster_timeseries import ( GenerateThermalClusterTimeSeries, ) @@ -38,11 +39,13 @@ from antarest.study.storage.variantstudy.model.command.remove_link import RemoveLink from antarest.study.storage.variantstudy.model.command.remove_renewables_cluster import RemoveRenewablesCluster from antarest.study.storage.variantstudy.model.command.remove_st_storage import RemoveSTStorage +from antarest.study.storage.variantstudy.model.command.remove_user_resource import RemoveUserResource from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix from antarest.study.storage.variantstudy.model.command.update_binding_constraint import UpdateBindingConstraint from antarest.study.storage.variantstudy.model.command.update_comments import UpdateComments from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig from antarest.study.storage.variantstudy.model.command.update_district import UpdateDistrict +from antarest.study.storage.variantstudy.model.command.update_link import UpdateLink from antarest.study.storage.variantstudy.model.command.update_playlist import UpdatePlaylist from antarest.study.storage.variantstudy.model.command.update_raw_file import UpdateRawFile from antarest.study.storage.variantstudy.model.command.update_scenario_builder import UpdateScenarioBuilder @@ -55,6 +58,7 @@ CommandName.CREATE_DISTRICT.value: CreateDistrict, CommandName.REMOVE_DISTRICT.value: RemoveDistrict, CommandName.CREATE_LINK.value: CreateLink, + CommandName.UPDATE_LINK.value: UpdateLink, CommandName.REMOVE_LINK.value: RemoveLink, CommandName.CREATE_BINDING_CONSTRAINT.value: CreateBindingConstraint, CommandName.UPDATE_BINDING_CONSTRAINT.value: UpdateBindingConstraint, @@ -73,6 +77,8 @@ CommandName.UPDATE_PLAYLIST.value: UpdatePlaylist, CommandName.UPDATE_SCENARIO_BUILDER.value: UpdateScenarioBuilder, CommandName.GENERATE_THERMAL_CLUSTER_TIMESERIES.value: GenerateThermalClusterTimeSeries, + CommandName.CREATE_USER_RESOURCE.value: CreateUserResource, + CommandName.REMOVE_USER_RESOURCE.value: RemoveUserResource, } diff --git a/antarest/study/storage/variantstudy/model/command/common.py b/antarest/study/storage/variantstudy/model/command/common.py index dc5dce4331..c2d604e55b 100644 --- a/antarest/study/storage/variantstudy/model/command/common.py +++ b/antarest/study/storage/variantstudy/model/command/common.py @@ -10,9 +10,12 @@ # # This file is part of the Antares project. +import typing as t from dataclasses import dataclass from enum import Enum +from antarest.study.storage.rawstudy.model.filesystem.root.user.user import User + @dataclass class CommandOutput: @@ -31,6 +34,7 @@ class CommandName(Enum): CREATE_DISTRICT = "create_district" REMOVE_DISTRICT = "remove_district" CREATE_LINK = "create_link" + UPDATE_LINK = "update_link" REMOVE_LINK = "remove_link" CREATE_BINDING_CONSTRAINT = "create_binding_constraint" UPDATE_BINDING_CONSTRAINT = "update_binding_constraint" @@ -49,3 +53,9 @@ class CommandName(Enum): UPDATE_PLAYLIST = "update_playlist" UPDATE_SCENARIO_BUILDER = "update_scenario_builder" GENERATE_THERMAL_CLUSTER_TIMESERIES = "generate_thermal_cluster_timeseries" + CREATE_USER_RESOURCE = "create_user_resource" + REMOVE_USER_RESOURCE = "remove_user_resource" + + +def is_url_writeable(user_node: User, url: t.List[str]) -> bool: + return url[0] not in [file.filename for file in user_node.registered_files] diff --git a/antarest/study/storage/variantstudy/model/command/create_link.py b/antarest/study/storage/variantstudy/model/command/create_link.py index 5090608820..1e46c20bf1 100644 --- a/antarest/study/storage/variantstudy/model/command/create_link.py +++ b/antarest/study/storage/variantstudy/model/command/create_link.py @@ -9,10 +9,13 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +from abc import ABCMeta from typing import Any, Dict, List, Optional, Tuple, Union, cast +from antares.study.version import StudyVersion from pydantic import ValidationInfo, field_validator, model_validator +from antarest.core.exceptions import LinkValidationError from antarest.core.utils.utils import assert_this from antarest.matrixstore.model import MatrixData from antarest.study.business.model.link_model import LinkInternal @@ -27,17 +30,11 @@ from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener from antarest.study.storage.variantstudy.model.model import CommandDTO +MATRIX_ATTRIBUTES = ["series", "direct", "indirect"] -class CreateLink(ICommand): - """ - Command used to create a link between two areas. - """ - - # Overloaded metadata - # =================== - command_name: CommandName = CommandName.CREATE_LINK - version: int = 1 +class AbstractLinkCommand(ICommand, metaclass=ABCMeta): + command_name: CommandName # Command parameters # ================== @@ -57,11 +54,133 @@ def validate_series( return validate_matrix(v, new_values) if v is not None else v @model_validator(mode="after") - def validate_areas(self) -> "CreateLink": + def validate_areas(self) -> "AbstractLinkCommand": if self.area1 == self.area2: raise ValueError("Cannot create link on same node") + + if self.study_version < STUDY_VERSION_8_2 and (self.direct is not None or self.indirect is not None): + raise LinkValidationError( + "The fields 'direct' and 'indirect' cannot be provided when the version is less than 820." + ) + return self + def to_dto(self) -> CommandDTO: + args = { + "area1": self.area1, + "area2": self.area2, + "parameters": self.parameters, + } + for attr in MATRIX_ATTRIBUTES: + if value := getattr(self, attr, None): + args[attr] = strip_matrix_protocol(value) + return CommandDTO(action=self.command_name.value, args=args, study_version=self.study_version) + + def match(self, other: ICommand, equal: bool = False) -> bool: + if not isinstance(other, self.__class__): + return False + simple_match = self.area1 == other.area1 and self.area2 == other.area2 + if not equal: + return simple_match + return ( + simple_match + and self.parameters == other.parameters + and self.series == other.series + and self.direct == other.direct + and self.indirect == other.indirect + ) + + def match_signature(self) -> str: + return str( + self.command_name.value + MATCH_SIGNATURE_SEPARATOR + self.area1 + MATCH_SIGNATURE_SEPARATOR + self.area2 + ) + + def _create_diff(self, other: "ICommand") -> List["ICommand"]: + other = cast(AbstractLinkCommand, other) + + commands: List[ICommand] = [] + area_from, area_to = sorted([self.area1, self.area2]) + if self.parameters != other.parameters: + properties = LinkInternal.model_validate(other.parameters or {}).model_dump( + mode="json", by_alias=True, exclude_none=True, exclude={"area1", "area2"} + ) + commands.append( + UpdateConfig( + target=f"input/links/{area_from}/properties/{area_to}", + data=properties, + command_context=self.command_context, + study_version=self.study_version, + ) + ) + if self.series != other.series: + commands.append( + ReplaceMatrix( + target=f"@links_series/{area_from}/{area_to}", + matrix=strip_matrix_protocol(other.series), + command_context=self.command_context, + study_version=self.study_version, + ) + ) + return commands + + def get_inner_matrices(self) -> List[str]: + list_matrices = [] + for attr in MATRIX_ATTRIBUTES: + if value := getattr(self, attr, None): + assert_this(isinstance(value, str)) + list_matrices.append(strip_matrix_protocol(value)) + return list_matrices + + def save_series(self, area_from: str, area_to: str, study_data: FileStudy, version: StudyVersion) -> None: + assert isinstance(self.series, str) + if version < STUDY_VERSION_8_2: + study_data.tree.save(self.series, ["input", "links", area_from, area_to]) + else: + study_data.tree.save( + self.series, + ["input", "links", area_from, f"{area_to}_parameters"], + ) + + def save_direct(self, area_from: str, area_to: str, study_data: FileStudy, version: StudyVersion) -> None: + assert isinstance(self.direct, str) + if version >= STUDY_VERSION_8_2: + study_data.tree.save( + self.direct, + [ + "input", + "links", + area_from, + "capacities", + f"{area_to}_direct", + ], + ) + + def save_indirect(self, area_from: str, area_to: str, study_data: FileStudy, version: StudyVersion) -> None: + assert isinstance(self.indirect, str) + if version >= STUDY_VERSION_8_2: + study_data.tree.save( + self.indirect, + [ + "input", + "links", + area_from, + "capacities", + f"{area_to}_indirect", + ], + ) + + +class CreateLink(AbstractLinkCommand): + """ + Command used to create a link between two areas. + """ + + # Overloaded metadata + # =================== + + command_name: CommandName = CommandName.CREATE_LINK + version: int = 1 + def _create_link_in_config(self, area_from: str, area_to: str, study_data: FileStudyTreeConfig) -> None: self.parameters = self.parameters or {} study_data.areas[area_from].links[area_to] = Link( @@ -158,115 +277,23 @@ def _apply(self, study_data: FileStudy, listener: Optional[ICommandListener] = N self.direct = self.direct or (self.command_context.generator_matrix_constants.get_link_direct()) self.indirect = self.indirect or (self.command_context.generator_matrix_constants.get_link_indirect()) - assert type(self.series) is str - if version < STUDY_VERSION_8_2: - study_data.tree.save(self.series, ["input", "links", area_from, area_to]) - else: - study_data.tree.save( - self.series, - ["input", "links", area_from, f"{area_to}_parameters"], - ) - - study_data.tree.save({}, ["input", "links", area_from, "capacities"]) - if self.direct: - assert isinstance(self.direct, str) - study_data.tree.save( - self.direct, - [ - "input", - "links", - area_from, - "capacities", - f"{area_to}_direct", - ], - ) - - if self.indirect: - assert isinstance(self.indirect, str) - study_data.tree.save( - self.indirect, - [ - "input", - "links", - area_from, - "capacities", - f"{area_to}_indirect", - ], - ) + self.save_series(area_from, area_to, study_data, version) + self.save_direct(area_from, area_to, study_data, version) + self.save_indirect(area_from, area_to, study_data, version) return output def to_dto(self) -> CommandDTO: - args = { - "area1": self.area1, - "area2": self.area2, - "parameters": self.parameters, - } - if self.series: - args["series"] = strip_matrix_protocol(self.series) - if self.direct: - args["direct"] = strip_matrix_protocol(self.direct) - if self.indirect: - args["indirect"] = strip_matrix_protocol(self.indirect) - return CommandDTO(action=CommandName.CREATE_LINK.value, args=args, study_version=self.study_version) + return super().to_dto() def match_signature(self) -> str: - return str( - self.command_name.value + MATCH_SIGNATURE_SEPARATOR + self.area1 + MATCH_SIGNATURE_SEPARATOR + self.area2 - ) + return super().match_signature() def match(self, other: ICommand, equal: bool = False) -> bool: - if not isinstance(other, CreateLink): - return False - simple_match = self.area1 == other.area1 and self.area2 == other.area2 - if not equal: - return simple_match - return ( - simple_match - and self.parameters == other.parameters - and self.series == other.series - and self.direct == other.direct - and self.indirect == other.indirect - ) + return super().match(other, equal) def _create_diff(self, other: "ICommand") -> List["ICommand"]: - other = cast(CreateLink, other) - - commands: List[ICommand] = [] - area_from, area_to = sorted([self.area1, self.area2]) - if self.parameters != other.parameters: - properties = LinkInternal.model_validate(other.parameters or {}) - link_property = properties.model_dump( - mode="json", by_alias=True, exclude_none=True, exclude={"area1", "area2"} - ) - commands.append( - UpdateConfig( - target=f"input/links/{area_from}/properties/{area_to}", - data=link_property, - command_context=self.command_context, - study_version=self.study_version, - ) - ) - if self.series != other.series: - commands.append( - ReplaceMatrix( - target=f"@links_series/{area_from}/{area_to}", - matrix=strip_matrix_protocol(other.series), - command_context=self.command_context, - study_version=self.study_version, - ) - ) - return commands + return super()._create_diff(other) def get_inner_matrices(self) -> List[str]: - list_matrices = [] - if self.series: - assert_this(isinstance(self.series, str)) - list_matrices.append(strip_matrix_protocol(self.series)) - if self.direct: - assert_this(isinstance(self.direct, str)) - list_matrices.append(strip_matrix_protocol(self.direct)) - if self.indirect: - assert_this(isinstance(self.indirect, str)) - list_matrices.append(strip_matrix_protocol(self.indirect)) - return list_matrices + return super().get_inner_matrices() diff --git a/antarest/study/storage/variantstudy/model/command/create_user_resource.py b/antarest/study/storage/variantstudy/model/command/create_user_resource.py new file mode 100644 index 0000000000..a62fd1f8b0 --- /dev/null +++ b/antarest/study/storage/variantstudy/model/command/create_user_resource.py @@ -0,0 +1,102 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +import typing as t +from enum import StrEnum + +from antarest.core.exceptions import ChildNotFoundError +from antarest.core.model import JSON +from antarest.core.serialization import AntaresBaseModel +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.user.user import User +from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput, is_url_writeable +from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand +from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener +from antarest.study.storage.variantstudy.model.model import CommandDTO + + +class ResourceType(StrEnum): + FILE = "file" + FOLDER = "folder" + + +class CreateUserResourceData(AntaresBaseModel): + path: str + resource_type: ResourceType + + +class CreateUserResource(ICommand): + """ + Command used to create a resource inside the `user` folder. + """ + + # Overloaded metadata + # =================== + + command_name: CommandName = CommandName.CREATE_USER_RESOURCE + version: int = 1 + + # Command parameters + # ================== + + data: CreateUserResourceData + + def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: + return CommandOutput(status=True, message="ok"), {} + + def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = None) -> CommandOutput: + url = [item for item in self.data.path.split("/") if item] + study_tree = study_data.tree + user_node = t.cast(User, study_tree.get_node(["user"])) + if not is_url_writeable(user_node, url): + return CommandOutput( + status=False, message=f"you are not allowed to create a resource here: {self.data.path}" + ) + try: + study_tree.get_node(["user"] + url) + except ChildNotFoundError: + # Creates the tree recursively to be able to create a resource inside a non-existing folder. + last_value = b"" if self.data.resource_type == ResourceType.FILE else {} + nested_dict: JSON = {url[-1]: last_value} + for key in reversed(url[:-1]): + nested_dict = {key: nested_dict} + study_tree.save({"user": nested_dict}) + else: + return CommandOutput(status=False, message=f"the given resource already exists: {self.data.path}") + return CommandOutput(status=True, message="ok") + + def to_dto(self) -> CommandDTO: + return CommandDTO( + action=self.command_name.value, + args={"data": self.data.model_dump(mode="json")}, + study_version=self.study_version, + ) + + def match_signature(self) -> str: + return str( + self.command_name.value + + MATCH_SIGNATURE_SEPARATOR + + self.data.path + + MATCH_SIGNATURE_SEPARATOR + + self.data.resource_type.value + ) + + def match(self, other: ICommand, equal: bool = False) -> bool: + if not isinstance(other, CreateUserResource): + return False + return self.data.path == other.data.path and self.data.resource_type == other.data.resource_type + + def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: + return [other] + + def get_inner_matrices(self) -> t.List[str]: + return [] diff --git a/antarest/study/storage/variantstudy/model/command/remove_user_resource.py b/antarest/study/storage/variantstudy/model/command/remove_user_resource.py new file mode 100644 index 0000000000..4f24041589 --- /dev/null +++ b/antarest/study/storage/variantstudy/model/command/remove_user_resource.py @@ -0,0 +1,84 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +import typing as t + +from antarest.core.exceptions import ChildNotFoundError +from antarest.core.serialization import AntaresBaseModel +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.user.user import User +from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput, is_url_writeable +from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand +from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener +from antarest.study.storage.variantstudy.model.model import CommandDTO + + +class RemoveUserResourceData(AntaresBaseModel): + path: str + + +class RemoveUserResource(ICommand): + """ + Command used to delete a resource inside the `user` folder. + """ + + # Overloaded metadata + # =================== + + command_name: CommandName = CommandName.REMOVE_USER_RESOURCE + version: int = 1 + + # Command parameters + # ================== + + data: RemoveUserResourceData + + def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: + return CommandOutput(status=True, message="ok"), {} + + def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = None) -> CommandOutput: + url = [item for item in self.data.path.split("/") if item] + study_tree = study_data.tree + user_node = t.cast(User, study_tree.get_node(["user"])) + if not is_url_writeable(user_node, url): + return CommandOutput( + status=False, message=f"you are not allowed to delete this resource : {self.data.path}" + ) + + try: + user_node.delete(url) + except ChildNotFoundError: + return CommandOutput(status=False, message="the given path doesn't exist") + + return CommandOutput(status=True, message="ok") + + def to_dto(self) -> CommandDTO: + return CommandDTO( + action=self.command_name.value, + args={"data": self.data.model_dump(mode="json")}, + study_version=self.study_version, + ) + + def match_signature(self) -> str: + return str(self.command_name.value + MATCH_SIGNATURE_SEPARATOR + self.data.path) + + def match(self, other: ICommand, equal: bool = False) -> bool: + if not isinstance(other, RemoveUserResource): + return False + return self.data.path == other.data.path + + def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: + return [other] + + def get_inner_matrices(self) -> t.List[str]: + return [] diff --git a/antarest/study/storage/variantstudy/model/command/update_link.py b/antarest/study/storage/variantstudy/model/command/update_link.py new file mode 100644 index 0000000000..ceab38ed56 --- /dev/null +++ b/antarest/study/storage/variantstudy/model/command/update_link.py @@ -0,0 +1,78 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +import typing as t + +from antarest.study.business.model.link_model import LinkInternal +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.variantstudy.model.command.common import CommandName, CommandOutput +from antarest.study.storage.variantstudy.model.command.create_link import AbstractLinkCommand +from antarest.study.storage.variantstudy.model.command.icommand import ICommand, OutputTuple +from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener +from antarest.study.storage.variantstudy.model.model import CommandDTO + + +class UpdateLink(AbstractLinkCommand): + """ + Command used to update a link between two areas. + """ + + # Overloaded metadata + # =================== + + command_name: CommandName = CommandName.UPDATE_LINK + version: int = 1 + + def _apply_config(self, study_data: FileStudyTreeConfig) -> OutputTuple: + return ( + CommandOutput( + status=True, + message=f"Link between '{self.area1}' and '{self.area2}' updated", + ), + {}, + ) + + def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = None) -> CommandOutput: + version = study_data.config.version + + properties = study_data.tree.get(["input", "links", self.area1, "properties", self.area2]) + + new_properties = LinkInternal.model_validate(self.parameters).model_dump(include=self.parameters, by_alias=True) + + properties.update(new_properties) + + study_data.tree.save(properties, ["input", "links", self.area1, "properties", self.area2]) + + output, _ = self._apply_config(study_data.config) + + if self.series: + self.save_series(self.area1, self.area2, study_data, version) + + if self.direct: + self.save_direct(self.area1, self.area2, study_data, version) + + if self.indirect: + self.save_indirect(self.area1, self.area2, study_data, version) + + return output + + def to_dto(self) -> CommandDTO: + return super().to_dto() + + def match_signature(self) -> str: + return super().match_signature() + + def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: + return super()._create_diff(other) + + def get_inner_matrices(self) -> t.List[str]: + return super().get_inner_matrices() diff --git a/antarest/study/web/raw_studies_blueprint.py b/antarest/study/web/raw_studies_blueprint.py index e3b114d6d5..fcab2738fd 100644 --- a/antarest/study/web/raw_studies_blueprint.py +++ b/antarest/study/web/raw_studies_blueprint.py @@ -39,6 +39,7 @@ from antarest.study.service import StudyService from antarest.study.storage.df_download import TableExportFormat, export_file from antarest.study.storage.rawstudy.model.filesystem.matrix.matrix import MatrixFrequency +from antarest.study.storage.variantstudy.model.command.create_user_resource import ResourceType try: import tables # type: ignore @@ -198,7 +199,7 @@ def delete_file( ) -> t.Any: uuid = sanitize_uuid(uuid) logger.info(f"Deleting path {path} inside study {uuid}", extra={"user": current_user.id}) - study_service.delete_file_or_folder(uuid, path, current_user) + study_service.delete_user_file_or_folder(uuid, path, current_user) @bp.get( "/studies/{uuid}/areas/aggregate/mc-ind/{output_id}", @@ -476,7 +477,7 @@ def aggregate_links_raw_data__all( "/studies/{uuid}/raw", status_code=http.HTTPStatus.NO_CONTENT, tags=[APITag.study_raw_data], - summary="Update data by posting formatted data", + summary="Update study by posting formatted data", ) def edit_study( uuid: str, @@ -493,13 +494,10 @@ def edit_study( - `uuid`: The UUID of the study. - `path`: The path to the data to update. Defaults to "/". - - `data`: The formatted data to be posted. Defaults to an empty string. - The data could be a JSON object, or a simple string. + - `data`: The formatted data to be posted. Could be a JSON object, or a string. Defaults to an empty string. + """ - logger.info( - f"Editing data at {path} for study {uuid}", - extra={"user": current_user.id}, - ) + logger.info(f"Editing data at {path} for study {uuid}", extra={"user": current_user.id}) path = sanitize_string(path) params = RequestParameters(user=current_user) study_service.edit_study(uuid, path, data, params) @@ -513,11 +511,12 @@ def edit_study( def replace_study_file( uuid: str, path: str = Param("/", examples=get_path_examples()), # type: ignore - file: bytes = File(...), + file: bytes = File(default=None), create_missing: bool = Query( False, description="Create file or parent directories if missing.", ), # type: ignore + resource_type: ResourceType = ResourceType.FILE, current_user: JWTUser = Depends(auth.get_current_user), ) -> None: """ @@ -528,15 +527,23 @@ def replace_study_file( - `uuid`: The UUID of the study. - `path`: The path to the data to update. Defaults to "/". - `file`: The raw file to be posted (e.g. a CSV file opened in binary mode). - - `create_missing`: Flag to indicate whether to create file or parent directories if missing. + - `create_missing`: Flag to indicate whether to create file and parent directories if missing. + - `resource_type`: When set to "folder" and `create_missing` is True, creates a folder. Else (default value), it's ignored. + """ - logger.info( - f"Uploading new data file at {path} for study {uuid}", - extra={"user": current_user.id}, - ) + if file is not None and resource_type == ResourceType.FOLDER: + raise HTTPException(status_code=422, detail="Argument mismatch: Cannot give a content to create a folder") + if file is None and resource_type == ResourceType.FILE: + raise HTTPException(status_code=422, detail="Argument mismatch: Must give a content to create a file") + path = sanitize_string(path) params = RequestParameters(user=current_user) - study_service.edit_study(uuid, path, file, params, create_missing=create_missing) + if resource_type == ResourceType.FOLDER and create_missing: # type: ignore + logger.info(f"Creating folder {path} for study {uuid}", extra={"user": current_user.id}) + study_service.create_user_folder(uuid, path, current_user) + else: + logger.info(f"Uploading new data file at {path} for study {uuid}", extra={"user": current_user.id}) + study_service.edit_study(uuid, path, file, params, create_missing=create_missing) @bp.get( "/studies/{uuid}/raw/validate", diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 6d94a48c54..9f267df774 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -68,7 +68,7 @@ ) from antarest.study.business.district_manager import DistrictCreationDTO, DistrictInfoDTO, DistrictUpdateDTO from antarest.study.business.general_management import GeneralFormFields -from antarest.study.business.model.link_model import LinkDTO +from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO from antarest.study.business.optimization_management import OptimizationFormFields from antarest.study.business.playlist_management import PlaylistColumns from antarest.study.business.scenario_builder_management import Rulesets, ScenarioType @@ -197,6 +197,26 @@ def create_link( params = RequestParameters(user=current_user) return study_service.create_link(uuid, link_creation_info, params) + @bp.put( + "/studies/{uuid}/links/{area_from}/{area_to}", + tags=[APITag.study_data], + summary="Update a link", + response_model=LinkDTO, + ) + def update_link( + uuid: str, + area_from: str, + area_to: str, + link_update_dto: LinkBaseDTO, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> t.Any: + logger.info( + f"Updating link {area_from} -> {area_to} for study {uuid}", + extra={"user": current_user.id}, + ) + params = RequestParameters(user=current_user) + return study_service.update_link(uuid, area_from, area_to, link_update_dto, params) + @bp.put( "/studies/{uuid}/areas/{area_id}/ui", tags=[APITag.study_data], diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9b9c729cf5..00c33a8f5a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,124 @@ Antares Web Changelog ===================== +v2.18.0 (2024-11-29) +------------------- + +## What's Changed + +### Features + +* **ui-common**: integrate `GlideDataGrid` into `MatrixGrid` [`2134`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2134) +* **pydantic**: use pydantic serialization [`2139`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2139) +* **aggregation-api**: delete index from the response file [`2151`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2151) +* **variant**: add new endpoint to clear snapshots [`2135`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2135) +* **version**: use class StudyVersion to handle versions [`2156`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2156) +* Increase cleaning snapshot frequency [`2173`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2173) +* **tests**: add tests on matrix index [`2180`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2180) +* **desktop**: open browser when server is started [`2187`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2187) +* **ui-tablemode**: prevent duplicate columns [`2190`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2190) +* **watcher**: filter out upgrade and TS generation `.tmp` folders [`2189`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2189) +* **installer**: update installer version and improve desktop version launcher [`2157`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2157) +* **tasks**: add new endpoint to fetch task progress [`2191`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2191) +* **bc**: use `update_config` instead of `update_bc` for multiple updates [`2105`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2105) +* **ts-gen**: display progress bar via websockets [`2194`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2194) +* **watcher**: add new endpoint for optimized scanning [`2193`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2193) +* **ui-ts**: update TimeSeriesManagement page to allow the generation of TS [`2170`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2170) +* **matrices**: allow csv import [`2211`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2211) +* **matrix**: allow import for various formats [`2218`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2218) +* **ui-results**: enhance results columns headers [`2207`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2207) +* **auto_archive_service**: increase cleaning snapshot frequency [`2213`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2213) +* **ui-study**: change error display in FreezeStudy [`2222`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2222) +* **ui-settings**: allow to change app language [`2226`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2226) +* **ui-results**: add column filters [`2230`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2230) +* **ts-gen**: add failing area and cluster info inside error msg (#2227) [`2231`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2231) +* Add new build directory structure [`2228`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2228) +* **installer**: update installer for new directory layout [`2242`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2242) +* **ui-studies**: allow to move an archived study [`2241`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2241) +* **ui-i18n**: change translations for thermal fields [`2246`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2246) + +### Bug Fixes + +* **ci**: multiply timeouts on windows platform [`2137`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2137) +* **playlist**: change response model to accept optional answers [`2152`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2152) +* **api**: allow `nominalcapacity` to be a float inside API response [`2158`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2158) +* **adq_patch**: set default value for field `enable-first-step` to False [`2160`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2160) +* **pydantic**: allow `str` fields to be populated by `int` [`2166`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2166) +* **api**: allow `min_stable_power` to be a float inside API response [`2167`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2167) +* **snapshot_cleaning**: set `ref_id` to `None` to prevent postgresql crash [`2169`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2169) +* **allocation**: show matrix even with only one area [`2168`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2168) +* Enable foreign keys for sqlite [`2172`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2172) +* **matrix-index**: return the right year [`2177`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2177) +* **tests**: adapt new year for index test [`2178`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2178) +* **db**: migrate db to use foreign key inside sqlite [`2185`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2185) +* Apidocs redirect [`2186`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2186) +* **bc**: display matrix index according to frequency [`2196`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2196) +* **docker**: reduce docker image size [`2195`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2195) +* **xpansion**: fix typo inside backend api call [`2197`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2197) +* **matrix**: return empty index for empty matrices [`2198`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2198) +* **archive**: raise Exception when (un)archiving wrong outputs [`2199`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2199) +* **installer**: update installer to fix install to wrong directory [`2205`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2205) +* **ts-gen**: add failing info in the front and fix pandas issue in the back [`2208`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2208) +* **ui-ws**: rename the task progress event type [`2209`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2209) +* **export**: allow digest file download [`2210`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2210) +* **ui-maps**: area positions are not saved [`2212`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2212) +* **ts-gen**: bump package to avoid `fo_rate` or `po_rate` exceptions [`2215`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2215) +* **ui**: progress bar issue [`2217`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2217) +* **ui-ts**: submit partial values instead of all [`2223`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2223) +* **ui-tasks**: add missing new task notifications [`2225`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2225) +* **ts-gen**: make variant generation fail when it's supposed to [`2234`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2234) +* **desktop,windows**: wait a few seconds for browser to open [`2247`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2247) +* **outputs**: allow reading inside archive + output with `.` in the name [`2249`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2249) +* **export**: allow export for zipped outputs [`2253`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2253) + +### Continuous Integration + +* **tests**: reduce number of workers for tests [`2149`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2149) + +### Documentation + +* Improve of the documentary tree and make some update [`2243`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2243) + +### Build + +* **python**: bump project dependencies [`1728`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1728) +* **ui**: fix rollup issue [`2161`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2161) +* **ui**: fix issue with build result not working [`2163`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2163) +* **deps**: bump launcher and paramiko versions [`2140`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2140) +* **python**: bump python version to use v3.11 [`2164`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2164) + +### Chore + +* **front-end**: add license headers inside front-end [`2145`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2145) +* **variants**: increase timeout duration for variant generation [`2144`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2144) +* **license**: add a new ESLint rule to check license header [`2150`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2150) + +### Perf + +* **scripts**: improve load balancing [`2165`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2165) + +### Style + +* **license**: reformat license header inside front-end [`2148`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2148) +* **api**: change apidoc example to make it work [`2155`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2155) +* **variant**: improve logs [`2179`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2179) + +### Refactor + +* **workers**: remove the `simulator` worker [`2184`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2184) +* **ui**: replace `MatrixInput` with `Matrix` Glide Data Grid integration [`2138`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2138) +* **aggregation-apis**: remove `time` column from the aggregated data [`2214`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2214) + +### Test + +* **ui-utils**: add tests for validation utils & refactor imports [`2192`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2192) + +### BREAKING CHANGES + +* **archive-apis**: use `.7z` format to archive studies [`2013`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/2013) + +**Full Changelog**: https://github.com/AntaresSimulatorTeam/AntaREST/compare/v2.17.6...v2.18.0 + v2.17.6 (2024-09-25) -------------------- diff --git a/docs/architecture/5-roadmap.md b/docs/architecture/5-roadmap.md deleted file mode 100644 index 28d9477bae..0000000000 --- a/docs/architecture/5-roadmap.md +++ /dev/null @@ -1 +0,0 @@ -#Roadmap \ No newline at end of file diff --git a/docs/assets/media/img/installer_screenshot.png b/docs/assets/media/img/installer_screenshot.png new file mode 100644 index 0000000000..dbb6c138c6 Binary files /dev/null and b/docs/assets/media/img/installer_screenshot.png differ diff --git a/docs/assets/media/img/userguide_change_language.png b/docs/assets/media/img/userguide_change_language.png new file mode 100644 index 0000000000..482f8fa688 Binary files /dev/null and b/docs/assets/media/img/userguide_change_language.png differ diff --git a/docs/assets/media/img/userguide_token_creation.png b/docs/assets/media/img/userguide_token_creation.png index 7d88161322..f383f909f7 100644 Binary files a/docs/assets/media/img/userguide_token_creation.png and b/docs/assets/media/img/userguide_token_creation.png differ diff --git a/docs/assets/media/img/userguide_token_listing.png b/docs/assets/media/img/userguide_token_listing.png index b88d73c59b..271fb771ff 100644 Binary files a/docs/assets/media/img/userguide_token_listing.png and b/docs/assets/media/img/userguide_token_listing.png differ diff --git a/docs/assets/media/img/userguide_token_result.png b/docs/assets/media/img/userguide_token_result.png index 623202e23d..971b9c5421 100644 Binary files a/docs/assets/media/img/userguide_token_result.png and b/docs/assets/media/img/userguide_token_result.png differ diff --git a/docs/assets/media/user-guide/simulation-configuration/01-configuration-general-tab.png b/docs/assets/media/user-guide/simulation-configuration/01-configuration-general-tab.png new file mode 100644 index 0000000000..9d5b8cc17c Binary files /dev/null and b/docs/assets/media/user-guide/simulation-configuration/01-configuration-general-tab.png differ diff --git a/docs/assets/media/user-guide/simulation-configuration/01-configuration-general-themmatic-trimming1.png b/docs/assets/media/user-guide/simulation-configuration/01-configuration-general-themmatic-trimming1.png new file mode 100644 index 0000000000..a12b4b8839 Binary files /dev/null and b/docs/assets/media/user-guide/simulation-configuration/01-configuration-general-themmatic-trimming1.png differ diff --git a/docs/assets/media/user-guide/simulation-configuration/01-configuration-general-themmatic-trimming2.png b/docs/assets/media/user-guide/simulation-configuration/01-configuration-general-themmatic-trimming2.png new file mode 100644 index 0000000000..ba40b75f33 Binary files /dev/null and b/docs/assets/media/user-guide/simulation-configuration/01-configuration-general-themmatic-trimming2.png differ diff --git a/docs/assets/media/user-guide/simulation-configuration/02-configuration-timeseries-management1.png b/docs/assets/media/user-guide/simulation-configuration/02-configuration-timeseries-management1.png new file mode 100644 index 0000000000..bab6242cf0 Binary files /dev/null and b/docs/assets/media/user-guide/simulation-configuration/02-configuration-timeseries-management1.png differ diff --git a/docs/assets/media/user-guide/simulation-configuration/02-configuration-timeseries-management2.png b/docs/assets/media/user-guide/simulation-configuration/02-configuration-timeseries-management2.png new file mode 100644 index 0000000000..1fa51672e8 Binary files /dev/null and b/docs/assets/media/user-guide/simulation-configuration/02-configuration-timeseries-management2.png differ diff --git a/docs/assets/media/user-guide/study/01-map-tab.png b/docs/assets/media/user-guide/study/01-map-tab.png new file mode 100644 index 0000000000..a8ee1b1daf Binary files /dev/null and b/docs/assets/media/user-guide/study/01-map-tab.png differ diff --git a/docs/assets/media/user-guide/study/01-map.tab.png b/docs/assets/media/user-guide/study/01-map.tab.png deleted file mode 100644 index 7890e89799..0000000000 Binary files a/docs/assets/media/user-guide/study/01-map.tab.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/02-areas-tab.png b/docs/assets/media/user-guide/study/02-areas-tab.png new file mode 100644 index 0000000000..2bb583cfc4 Binary files /dev/null and b/docs/assets/media/user-guide/study/02-areas-tab.png differ diff --git a/docs/assets/media/user-guide/study/02-areas.tab.png b/docs/assets/media/user-guide/study/02-areas.tab.png deleted file mode 100644 index 94e2b29a15..0000000000 Binary files a/docs/assets/media/user-guide/study/02-areas.tab.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/03-links-tab.png b/docs/assets/media/user-guide/study/03-links-tab.png new file mode 100644 index 0000000000..3d1670bee9 Binary files /dev/null and b/docs/assets/media/user-guide/study/03-links-tab.png differ diff --git a/docs/assets/media/user-guide/study/03-links.tab.png b/docs/assets/media/user-guide/study/03-links.tab.png deleted file mode 100644 index 0f6771de6d..0000000000 Binary files a/docs/assets/media/user-guide/study/03-links.tab.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/04-binding-constraints-tab.png b/docs/assets/media/user-guide/study/04-binding-constraints-tab.png new file mode 100644 index 0000000000..40569ada9f Binary files /dev/null and b/docs/assets/media/user-guide/study/04-binding-constraints-tab.png differ diff --git a/docs/assets/media/user-guide/study/04-binding-constraints.tab.png b/docs/assets/media/user-guide/study/04-binding-constraints.tab.png deleted file mode 100644 index 4fdae52ef5..0000000000 Binary files a/docs/assets/media/user-guide/study/04-binding-constraints.tab.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/05-debug-tab.png b/docs/assets/media/user-guide/study/05-debug-tab.png new file mode 100644 index 0000000000..7b764e2d20 Binary files /dev/null and b/docs/assets/media/user-guide/study/05-debug-tab.png differ diff --git a/docs/assets/media/user-guide/study/05-debug.tab.png b/docs/assets/media/user-guide/study/05-debug.tab.png deleted file mode 100644 index 59ae230307..0000000000 Binary files a/docs/assets/media/user-guide/study/05-debug.tab.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/06-table-mode-tab.png b/docs/assets/media/user-guide/study/06-table-mode-tab.png new file mode 100644 index 0000000000..7f2d417609 Binary files /dev/null and b/docs/assets/media/user-guide/study/06-table-mode-tab.png differ diff --git a/docs/assets/media/user-guide/study/06-table-mode.tab.png b/docs/assets/media/user-guide/study/06-table-mode.tab.png deleted file mode 100644 index 064e2b9b82..0000000000 Binary files a/docs/assets/media/user-guide/study/06-table-mode.tab.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/07-map-layers-districts.png b/docs/assets/media/user-guide/study/07-map-layers-districts.png new file mode 100644 index 0000000000..aa44f2c0cb Binary files /dev/null and b/docs/assets/media/user-guide/study/07-map-layers-districts.png differ diff --git a/docs/assets/media/user-guide/study/08-layers-districts.png b/docs/assets/media/user-guide/study/08-layers-districts.png new file mode 100644 index 0000000000..523df53a52 Binary files /dev/null and b/docs/assets/media/user-guide/study/08-layers-districts.png differ diff --git a/docs/assets/media/user-guide/study/09-results-view.png b/docs/assets/media/user-guide/study/09-results-view.png new file mode 100644 index 0000000000..34641ff906 Binary files /dev/null and b/docs/assets/media/user-guide/study/09-results-view.png differ diff --git a/docs/assets/media/user-guide/study/areas/01-properties-form.png b/docs/assets/media/user-guide/study/areas/01-properties-form.png index 55c3afe312..1de7bae70f 100644 Binary files a/docs/assets/media/user-guide/study/areas/01-properties-form.png and b/docs/assets/media/user-guide/study/areas/01-properties-form.png differ diff --git a/docs/assets/media/user-guide/study/areas/03-thermals-form.png b/docs/assets/media/user-guide/study/areas/03-thermals-form.png new file mode 100644 index 0000000000..f7ba893ac4 Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/03-thermals-form.png differ diff --git a/docs/assets/media/user-guide/study/areas/03-thermals-series.png b/docs/assets/media/user-guide/study/areas/03-thermals-series.png new file mode 100644 index 0000000000..80ad1ec135 Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/03-thermals-series.png differ diff --git a/docs/assets/media/user-guide/study/areas/03-thermals.form.png b/docs/assets/media/user-guide/study/areas/03-thermals.form.png deleted file mode 100644 index ac431175c3..0000000000 Binary files a/docs/assets/media/user-guide/study/areas/03-thermals.form.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/areas/03-thermals.series.png b/docs/assets/media/user-guide/study/areas/03-thermals.series.png deleted file mode 100644 index 90fbca354b..0000000000 Binary files a/docs/assets/media/user-guide/study/areas/03-thermals.series.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/areas/04-renewables-activation.png b/docs/assets/media/user-guide/study/areas/04-renewables-activation.png new file mode 100644 index 0000000000..027dfe11c1 Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/04-renewables-activation.png differ diff --git a/docs/assets/media/user-guide/study/areas/05-hydro-allocation.png b/docs/assets/media/user-guide/study/areas/05-hydro-allocation.png new file mode 100644 index 0000000000..b0c2b59af2 Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/05-hydro-allocation.png differ diff --git a/docs/assets/media/user-guide/study/areas/05-hydro-correlation.png b/docs/assets/media/user-guide/study/areas/05-hydro-correlation.png new file mode 100644 index 0000000000..c96dc6bc04 Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/05-hydro-correlation.png differ diff --git a/docs/assets/media/user-guide/study/areas/05-hydro-dailypower-energycredits.png b/docs/assets/media/user-guide/study/areas/05-hydro-dailypower-energycredits.png new file mode 100644 index 0000000000..30d4786f67 Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/05-hydro-dailypower-energycredits.png differ diff --git a/docs/assets/media/user-guide/study/areas/05-hydro-inflowstructure.png b/docs/assets/media/user-guide/study/areas/05-hydro-inflowstructure.png new file mode 100644 index 0000000000..8d0d01a365 Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/05-hydro-inflowstructure.png differ diff --git a/docs/assets/media/user-guide/study/areas/06-wind-activation.png b/docs/assets/media/user-guide/study/areas/06-wind-activation.png new file mode 100644 index 0000000000..008af0955f Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/06-wind-activation.png differ diff --git a/docs/assets/media/user-guide/study/areas/08-st-storages-form.png b/docs/assets/media/user-guide/study/areas/08-st-storages-form.png new file mode 100644 index 0000000000..ade8f46600 Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/08-st-storages-form.png differ diff --git a/docs/assets/media/user-guide/study/areas/08-st-storages-list-enable.png b/docs/assets/media/user-guide/study/areas/08-st-storages-list-enable.png new file mode 100644 index 0000000000..ecca702e28 Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/08-st-storages-list-enable.png differ diff --git a/docs/assets/media/user-guide/study/areas/08-st-storages-list.png b/docs/assets/media/user-guide/study/areas/08-st-storages-list.png new file mode 100644 index 0000000000..fe838f592e Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/08-st-storages-list.png differ diff --git a/docs/assets/media/user-guide/study/areas/08-st-storages-series.png b/docs/assets/media/user-guide/study/areas/08-st-storages-series.png new file mode 100644 index 0000000000..5852d8953d Binary files /dev/null and b/docs/assets/media/user-guide/study/areas/08-st-storages-series.png differ diff --git a/docs/assets/media/user-guide/study/areas/08-st-storages.form.png b/docs/assets/media/user-guide/study/areas/08-st-storages.form.png deleted file mode 100644 index a7be6236c1..0000000000 Binary files a/docs/assets/media/user-guide/study/areas/08-st-storages.form.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/areas/08-st-storages.list.png b/docs/assets/media/user-guide/study/areas/08-st-storages.list.png deleted file mode 100644 index 213befc8e3..0000000000 Binary files a/docs/assets/media/user-guide/study/areas/08-st-storages.list.png and /dev/null differ diff --git a/docs/assets/media/user-guide/study/areas/08-st-storages.series.png b/docs/assets/media/user-guide/study/areas/08-st-storages.series.png deleted file mode 100644 index c8cfba6194..0000000000 Binary files a/docs/assets/media/user-guide/study/areas/08-st-storages.series.png and /dev/null differ diff --git a/docs/architecture/2-add-new-antares-version.md b/docs/developer-guide/2-add-new-antares-version.md similarity index 100% rename from docs/architecture/2-add-new-antares-version.md rename to docs/developer-guide/2-add-new-antares-version.md diff --git a/docs/developer-guide/5-roadmap.md b/docs/developer-guide/5-roadmap.md new file mode 100644 index 0000000000..2ebfeb47d7 --- /dev/null +++ b/docs/developer-guide/5-roadmap.md @@ -0,0 +1,2 @@ +# Roadmap +**_This section is under construction_** \ No newline at end of file diff --git a/docs/architecture/0-introduction.md b/docs/developer-guide/architecture/0-introduction.md similarity index 100% rename from docs/architecture/0-introduction.md rename to docs/developer-guide/architecture/0-introduction.md diff --git a/docs/architecture/1-database.md b/docs/developer-guide/architecture/1-database.md similarity index 100% rename from docs/architecture/1-database.md rename to docs/developer-guide/architecture/1-database.md diff --git a/docs/install/0-INSTALL.md b/docs/developer-guide/install/0-INSTALL.md similarity index 100% rename from docs/install/0-INSTALL.md rename to docs/developer-guide/install/0-INSTALL.md diff --git a/docs/install/1-CONFIG.md b/docs/developer-guide/install/1-CONFIG.md similarity index 100% rename from docs/install/1-CONFIG.md rename to docs/developer-guide/install/1-CONFIG.md diff --git a/docs/install/2-DEPLOY.md b/docs/developer-guide/install/2-DEPLOY.md similarity index 85% rename from docs/install/2-DEPLOY.md rename to docs/developer-guide/install/2-DEPLOY.md index 1136e24410..87df158b79 100644 --- a/docs/install/2-DEPLOY.md +++ b/docs/developer-guide/install/2-DEPLOY.md @@ -68,27 +68,28 @@ These steps should work on any linux system with docker and docker-compose insta This is an example of a deployment. You'll have to edit your own `docker-compose.yml` file and [`application.yaml` configuration](./1-CONFIG.md) to customize it to your needs. -## Local application build +## Local desktop application The local application is a bundled build of the web server to ease its launch as a kind of desktop application. -When started, the application will be shown as a systray application (icon in the bottom right corner of the Windows bar). The menu will allow to go -to the local address where the interface is available. +When started, the application will be shown as a systray application (icon in the bottom right corner of the Windows bar). The menu will allow to go to the local address where the interface is available. The build is directly available in the [release](https://github.com/AntaresSimulatorTeam/AntaREST/releases) files for each version. You can download the latest version here: -- [For Windows](https://github.com/AntaresSimulatorTeam/AntaREST/releases/download/v2.5.0/AntaresWeb-windows-latest.zip) -- [For Ubuntu](https://github.com/AntaresSimulatorTeam/AntaREST/releases/download/v2.5.0/AntaresWeb-ubuntu-latest.zip) +- [For Windows](https://github.com/AntaresSimulatorTeam/AntaREST/releases/download/v2.18.0/AntaresWeb-windows-v2.18.0.zip) +- [For Ubuntu 20.04 ](https://github.com/AntaresSimulatorTeam/AntaREST/releases/download/v2.18.0/AntaresWeb-ubuntu_20.04-v2.18.0.zip) +- [For Ubuntu 22.04](https://github.com/AntaresSimulatorTeam/AntaREST/releases/download/v2.18.0/AntaresWeb-ubuntu_22.04-v2.18.0.zip) The local application contains an installer program that you can use to manage the installation. This program will keep your local data while updating the functional parts of the server. ### GUI installer (windows only) -Double-click on the installer executable and follow the instructions. -In case you already have a local application, choose your current application path -in order to update it. +Double-click on the AntaresWebInstaller.exe executable and follow the instructions. +In case you already have a local application, choose your current application path in order to update it. + +![installer_screenshot.png](../../assets/media/img/installer_screenshot.png) ### CLI installer (linux only) diff --git a/docs/index.md b/docs/index.md index 4e1a2f818f..f7475b6d17 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,9 +33,9 @@ This integration brings: ## Documentation -- [Building the application](./install/0-INSTALL.md) +- [Building the application](./developer-guide/install/0-INSTALL.md) - [Using the application](./user-guide/0-introduction.md) -- [Contributing to the application code](./architecture/0-introduction.md) +- [Contributing to the application code](./developer-guide/architecture/0-introduction.md) `Antares-Web` is currently under development. Feel free to submit any issue. diff --git a/docs/user-guide/0-introduction.md b/docs/user-guide/0-introduction.md index de2bd50eb7..539ebb88bc 100644 --- a/docs/user-guide/0-introduction.md +++ b/docs/user-guide/0-introduction.md @@ -24,7 +24,7 @@ It enables detailed modeling of energy consumption, generation, and transportati across numerous year-long scenarios, each consisting of 8760 hourly time-frames. `antares-web` serves as a server API interfacing with Antares Simulator studies, providing a web application to manage -studies while adding features for enhanced edition capabilities. +studies while adding features for enhanced edition capabilities. It's also can be use in a local desktop application for one user, find the instruction about it [here](../developer-guide/install/2-DEPLOY.md#Local-application-build). This integration brings: diff --git a/docs/user-guide/1-interface.md b/docs/user-guide/1-interface.md index 38d1088a0c..b31ce600ed 100644 --- a/docs/user-guide/1-interface.md +++ b/docs/user-guide/1-interface.md @@ -14,11 +14,30 @@ tags: - dataset - batch - launch + - settings --- # User interface -## What's new (2.5.0) +## What's new (2.18.0) +- redesign of the debug view, with the possibility of importing files into the user folder. See more details [here](./study/05-debug.md) +- new endpoint for admin to clear snapshots +- update version of python to 3.11 +- time series generator for thermals clusters. See more details [here](../user-guide/simulation-configuration/all-configurations.md#time-series-management) +- installer for desktop version of Antares Web +- use .7z format to export and archive studies +- new component for matrix (Data Glide) +- adding column filters and search bar on results view +- enhance results columns headers +- adding aggregates columns on some matrix +- disable copy/paste on matrix +- allow csv import +- allow to change app language. See more details [here](#tabs-description) + +Antares Web is supporting antares simulator version untill v8.8.x. +For more details, see the changelog. + +## Main features of the interface - [Launch batch mode](#launch-batch-mode) - [Strict folder filtering](#strict-folder-filtering) @@ -146,9 +165,24 @@ The data which can be uploaded are either a single tsv file, or a zipped list of ![](../assets/media/img/userguide_dataset_creation.png) -## User account and api tokens +## Settings + +The settings are accessible in the menu on the left of the application. There are a total of 5 tabs that are visible depending on the user profile: +- the normal user has access to GENERAL and TOKENS tabs +- the administrator user of a group has access to the GROUPS tab in addition to the normal user tabs +- the server administrator has access to all tabs (USERS and MAINTENANCE tabs in addition to the others). + +### Tabs description +1. GENERAL: allows you to change the language of the application, two possible choices English or French +2. USERS: List of users on the server, you can create, delete or modify their permissions +3. GROUPS: List of user groups on the server, you can create, delete, modify its list of users or their permissions in the group +4. TOKENS: Allows you to create a token that will be used to access to APIs via scripts +5. MAINTENANCE: Allows you to put the server in maintenance mode, preventing other users other than the administrator from accessing the application. + +![userguide_change_language.png](../assets/media/img/userguide_change_language.png) + +### Create a token -For normal user, the account section allows the creation of "api token". These token can be used in scripts that will use the [API](#api-documentation). ![](../assets/media/img/userguide_token_listing.png) diff --git a/docs/user-guide/2-study.md b/docs/user-guide/2-study.md index 2db09dbbf4..a7f34715bb 100644 --- a/docs/user-guide/2-study.md +++ b/docs/user-guide/2-study.md @@ -1,5 +1,5 @@ --- -title: Study Configuration +title: Study Modelization author: Antares Web Team date: 2023-12-21 category: User Guide @@ -11,15 +11,13 @@ tags: - areas - links - binding-constraints - - debug - - table-mode --- -# Study Configuration +# Study Modelization -This page is dedicated to configuring the study in the Antares Web application. +This page is dedicated to modelize a study in the Antares Web application. -To access the configuration of the study: +To access the modelization of the study: 1. From the "Study" view, click on the "MODELIZATION" tab. @@ -29,5 +27,3 @@ To access the configuration of the study: - [Areas](study/02-areas.md) - [Links](study/03-links.md) - [Binding Constraints](study/04-binding-constraints.md) -- [Debug](study/05-debug.md) -- [Table Mode](study/06-table-mode.md) diff --git a/docs/how-to/studies-create.md b/docs/user-guide/how-to/studies-create.md similarity index 92% rename from docs/how-to/studies-create.md rename to docs/user-guide/how-to/studies-create.md index 8123872433..10e7a18d07 100644 --- a/docs/how-to/studies-create.md +++ b/docs/user-guide/how-to/studies-create.md @@ -24,7 +24,7 @@ This guide will walk you through the steps to create a new study, from initial s Navigate to the "Studies" pages to display the list of studies. Click on the "Create" button to open the "Create Study" dialog box. -![List of studies](../assets/media/how-to/study-create-button.png) +![List of studies](../../assets/media/how-to/study-create-button.png) ## Fill in Study Properties @@ -40,7 +40,7 @@ In the "Create Study" dialog, you will be prompted to enter details about your s - **Tag**: Add tags to your study to help categorize and organize it (enter each tag, and presse Ctrl+Enter to add it). -![Create Study Form](../assets/media/how-to/study-create-form.png) +![Create Study Form](../../assets/media/how-to/study-create-form.png) Validate the form by clicking the "Save" button. @@ -54,7 +54,7 @@ metadata. Select the `default` folder to display the list of studies in this folder. Click on the "More options" button of your study to access the "Properties" and "Move" options. -![Other Options Menu](../assets/media/how-to/study-create-other-options.png) +![Other Options Menu](../../assets/media/how-to/study-create-other-options.png) ### Move Study in a Subfolder @@ -63,7 +63,7 @@ The "Move Study" dialog opens, allowing you to select the destination folder. Enter the name of the subfolder and validate by clicking the "Save" button. -![Move Study Dialog](../assets/media/how-to/study-creation-move-to-subfolder.png) +![Move Study Dialog](../../assets/media/how-to/study-creation-move-to-subfolder.png) If the subfolder does not exist, it will be created automatically. @@ -81,7 +81,7 @@ The "Edit Study" dialog opens, allowing you to modify the study name, permission - **Tag**: Add tags to your study to help categorize and organize it (enter each tag, and presse Ctrl+Enter to add it). -![Edit Study Dialog](../assets/media/how-to/study-creation-edit-properties.png) +![Edit Study Dialog](../../assets/media/how-to/study-creation-edit-properties.png) Validate the form by clicking the "Save" button. @@ -151,7 +151,7 @@ Here is a breakdown of what each part of the code does: See also: -- ["User account & api tokens"](../user-guide/1-interface.md#user-account-and-api-tokens) in the user guide. +- ["User account & api tokens"](../1-interface.md#user-account-and-api-tokens) in the user guide. ## See also diff --git a/docs/how-to/studies-import.md b/docs/user-guide/how-to/studies-import.md similarity index 100% rename from docs/how-to/studies-import.md rename to docs/user-guide/how-to/studies-import.md diff --git a/docs/how-to/studies-upgrade.md b/docs/user-guide/how-to/studies-upgrade.md similarity index 100% rename from docs/how-to/studies-upgrade.md rename to docs/user-guide/how-to/studies-upgrade.md diff --git a/docs/user-guide/simulation-configuration/all-configurations.md b/docs/user-guide/simulation-configuration/all-configurations.md new file mode 100644 index 0000000000..8f9739fbe6 --- /dev/null +++ b/docs/user-guide/simulation-configuration/all-configurations.md @@ -0,0 +1,100 @@ +--- +title: Simulation Configuration +author: Antares Web Team +date: 2024-11-22 +category: User Guide +tags: + + - simulation configuration + - monte-carlo scenarios + - output profile + - calendar + - optimization preferences + +--- + +# Simulation Configuration +**_This section is under construction_** + +## Introduction + +This documentation is dedicated to configuring simulation in the Antares Web application. + +To access the configuration of simulation, from the "Study" view, click on the "CONFIGURATION" tab. + +All the configuration are here. + +## General configuration +To access the general configuration of a study : + +1. From the "Study" view, click on the "CONFIGURATION" tab. +2. The firt next tab is for general configuration. + +![01-configuration-general-tab.png](../../assets/media/user-guide/simulation-configuration/01-configuration-general-tab.png) + +Let’s focus on some options in this section. + +### Building mode +> TODO + +### Selection mode +> TODO + +### Geographic trimming +> TODO + +### Thematic trimming + +To access to the thematic trimming, when you are in general configuration : +1. Go to Output profile +2. In Thematic trimming drop-down list, choose "Custom" + +![01-configuration-general-themmatic-trimming1.png](../../assets/media/user-guide/simulation-configuration/01-configuration-general-themmatic-trimming1.png) + +3. The SETTING option just to the right will be activated, click on it to access the thematic trimming configuration + +![01-configuration-general-themmatic-trimming2.png](../../assets/media/user-guide/simulation-configuration/01-configuration-general-themmatic-trimming2.png) + +The output variables are grouped by modeling objects (links, thermal clusters, etc.). + +On the thematic trimming window, you will find the following elements: + +- **Search bar:** You can search for specific variables, this will return all variables that match the criteria you entered in the search bar. +- **Selection option:** You will find options allowing you to activate or deactivate all the variables or even to fold all the variable groups. You can unfold a specific group manually. + +The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/stable/user-guide/ts-generator/04-parameters/#general-parameters). + +## Time-series management + +To access to Time-series management : +1. From the "Study" view, click on the "CONFIGURATION" tab +2. The second next tab is for time series management. + +![01-configuration-timeseries-management1.png](../../assets/media/user-guide/simulation-configuration/02-configuration-timeseries-management1.png) + +In this view you can generate time series for all thermals clusters. Choose "To be Generated" and set the number stochastics TS you want. And clic on GENERATE TS. + +![01-configuration-timeseries-management2.png](../../assets/media/user-guide/simulation-configuration/02-configuration-timeseries-management2.png) + +It'll create a task you can monitor on the Tasks view. The TS generate are visible on Availability tab of the thermal cluster of the study. + +A new library [antares-timeseries-generation](https://github.com/AntaresSimulatorTeam/antares-timeseries-generation) was created to generate time series for clusters. + +## Optimization preferences +> TODO + +## Adequacy Patch +> TODO + +## Advanced parameters +> TODO + +## Economic Opt. +> TODO + +## Geographic Trimming (Areas) +> TODO +## Geographic Trimming (Links) +> TODO +## Geographic Trimming (Binding Constraints) +> TODO diff --git a/docs/user-guide/study/01-map.md b/docs/user-guide/study/01-map.md index 8cbfc94685..74517d1efd 100644 --- a/docs/user-guide/study/01-map.md +++ b/docs/user-guide/study/01-map.md @@ -1,6 +1,19 @@ -# Study Map +--- +title: Study Map +author: Antares Web Team +date: 2024-11-21 +category: User Guide +tags: + + - simulation modelization + - map + - links + - areas + - layers + - districts -[⬅ Study Configuration](../2-study.md) +--- +# Study Map This page allows you to sketch the production and consumption network of your study. You can add, edit, and delete nodes and links. @@ -10,4 +23,16 @@ To access the map of the study: 1. From the "Study" view, click on the "MODELIZATION" tab. 2. Click on the "MAP" tab to access the page dedicated to the map. -![01-map.tab.png](../../assets/media/user-guide/study/01-map.tab.png) +![01-map-tab.png](../../assets/media/user-guide/study/01-map-tab.png) + +## Layers and districts +You can also add, edit and deleate layers and districts here. +Go to bottom right of the web page and clic on the gear icon. + +![07-map-layers-districts.png](../../assets/media/user-guide/study/07-map-layers-districts.png) + +And this page will be opened. To delete a layer ou a district, clic on Edit. + +![08-layers-districts.png](../../assets/media/user-guide/study/08-layers-districts.png) + +[⬅ Back to Study Modelization](../2-study.md) \ No newline at end of file diff --git a/docs/user-guide/study/02-areas.md b/docs/user-guide/study/02-areas.md index 10cb737302..df8a83db28 100644 --- a/docs/user-guide/study/02-areas.md +++ b/docs/user-guide/study/02-areas.md @@ -1,7 +1,5 @@ # Area Configuration -[⬅ Study Configuration](../2-study.md) - This page is dedicated to configuring areas in the Antares Web application. To access the configuration of areas: @@ -9,7 +7,7 @@ To access the configuration of areas: 1. From the "Study" view, click on the "MODELIZATION" tab. 2. Click on the "AREAS" tab to access the page dedicated to areas. -![02-areas.tab.png](../../assets/media/user-guide/study/02-areas.tab.png) +![02-areas-tab.png](../../assets/media/user-guide/study/02-areas-tab.png) ## Main Topics @@ -23,3 +21,5 @@ To access the configuration of areas: - [Short-Term Storages](areas/08-st-storages.md) - [Reserves](areas/09-reserves.md) - [Miscellaneous Generators](areas/10-misc-gen.md) + +[⬅ Study Configuration](../2-study.md) diff --git a/docs/user-guide/study/03-links.md b/docs/user-guide/study/03-links.md index 1c53563170..e63ba273ac 100644 --- a/docs/user-guide/study/03-links.md +++ b/docs/user-guide/study/03-links.md @@ -1,7 +1,5 @@ # Links Configuration -[⬅ Study Configuration](../2-study.md) - This page is dedicated to configuring links in the Antares Web application. To access the configuration of areas: @@ -9,5 +7,7 @@ To access the configuration of areas: 1. From the "Study" view, click on the "MODELIZATION" tab. 2. Click on the "AREAS" tab to access the page dedicated to links. -![03-links.tab.png](../../assets/media/user-guide/study/03-links.tab.png) +![03-links-tab.png](../../assets/media/user-guide/study/03-links-tab.png) + +[⬅ Back to Study Configuration](../2-study.md) diff --git a/docs/user-guide/study/04-binding-constraints.md b/docs/user-guide/study/04-binding-constraints.md index ff0b78c926..c6948909f0 100644 --- a/docs/user-guide/study/04-binding-constraints.md +++ b/docs/user-guide/study/04-binding-constraints.md @@ -1,7 +1,5 @@ # Binding Constraints Configuration -[⬅ Study Configuration](../2-study.md) - This page is dedicated to configuring binding constraints in the Antares Web application. To access the configuration of binding constraints: @@ -9,4 +7,6 @@ To access the configuration of binding constraints: 1. From the "Study" view, click on the "MODELIZATION" tab. 2. Click on the "BINDING CONSTRAINTS" tab to access the page dedicated to binding constraints. -![04-binding-constraints.tab.png](../../assets/media/user-guide/study/04-binding-constraints.tab.png) +![04-binding-constraints-tab.png](../../assets/media/user-guide/study/04-binding-constraints-tab.png) + +[⬅ Back to Study Configuration](../2-study.md) diff --git a/docs/user-guide/study/05-debug.md b/docs/user-guide/study/05-debug.md index 8c2caaaa44..65a2722c68 100644 --- a/docs/user-guide/study/05-debug.md +++ b/docs/user-guide/study/05-debug.md @@ -1,12 +1,12 @@ # Debug View - -[⬅ Study Configuration](../2-study.md) +**_This section is under construction_** This page is dedicated to the debugging of the study in the Antares Web application. To access the debug view: -1. From the "Study" view, click on the "MODELIZATION" tab. -2. Click on the "DEBUG" tab to access the page dedicated to debugging. +1. From the "Study" view, click on the "DEBUG" tab to access the page dedicated to debugging. + +![05-debug-tab.png](../../assets/media/user-guide/study/05-debug-tab.png) -![05-debug.tab.png](../../assets/media/user-guide/study/05-debug.tab.png) +[⬅ Back to Study Configuration](../2-study.md) \ No newline at end of file diff --git a/docs/user-guide/study/06-table-mode.md b/docs/user-guide/study/06-table-mode.md index 53c97ecea2..7727c9ab72 100644 --- a/docs/user-guide/study/06-table-mode.md +++ b/docs/user-guide/study/06-table-mode.md @@ -1,12 +1,14 @@ # Table Mode -[⬅ Study Configuration](../2-study.md) - This page is dedicated to the table mode, which allows you to edit the study properties in a tables. To access the table mode: -1. From the "Study" view, click on the "MODELIZATION" tab. -2. Click on the "TABLE MODE" tab to access the page dedicated to the table mode. +1. From the "Study" view, click on the "TABLE MODE" tab to access the page dedicated to the table mode. + +![06-table-mode-tab.png](../../assets/media/user-guide/study/06-table-mode-tab.png) + +**_This section is under construction_** +> TODO : Describe here the operation and use of modeling tables. -![06-table-mode.tab.png](../../assets/media/user-guide/study/06-table-mode.tab.png) +[⬅ Back to Study Configuration](../2-study.md) \ No newline at end of file diff --git a/docs/user-guide/study/07-xpansion.md b/docs/user-guide/study/07-xpansion.md new file mode 100644 index 0000000000..b88fafa754 --- /dev/null +++ b/docs/user-guide/study/07-xpansion.md @@ -0,0 +1,14 @@ +--- +title: Xpansion +author: Antares Web Team +date: 2024-11-22 +category: User Guide +tags: + + - xpansion + +--- + +# Xpansion +**_This section is under construction_** +> TODO : describe here the xpansion view \ No newline at end of file diff --git a/docs/user-guide/study/08-results.md b/docs/user-guide/study/08-results.md new file mode 100644 index 0000000000..5fb22cd8b5 --- /dev/null +++ b/docs/user-guide/study/08-results.md @@ -0,0 +1,33 @@ +--- +title: Results +author: Antares Web Team +date: 2024-11-22 +category: User Guide +tags: + + - results + - output + - simulation results + +--- + +# Results +**_This section is under construction_** + +This page is dedicated to the results of a simulation. + +To access results : + +1. From the "Study" view, click on the "RESULTS" tab. + +![09-results-view.png](../../assets/media/user-guide/study/09-results-view.png) + +On the results page, you will find the following elements: + +- **Toolbar:** Use the toolbar to filter and quickly search in the matrix results +- **Monte-Carlo filter output profile:** choose between Synthesis ou Year-by-year output +- **Display:** choose betwen Geneal values, Thermal plants, Ren. clusters, RecordYear ou ST Storages +- **Temporality:** choose between Hourly, Daily, Weekly, Monthly or Annuel +- **EXPORT button:** You can choose to export a specfic output matrix in TSV or Excel format. + +[⬅ Back to Study Configuration](../2-study.md) \ No newline at end of file diff --git a/docs/user-guide/study/areas/01-properties.md b/docs/user-guide/study/areas/01-properties.md index f5605ba153..5db1c5b2ec 100644 --- a/docs/user-guide/study/areas/01-properties.md +++ b/docs/user-guide/study/areas/01-properties.md @@ -1,7 +1,5 @@ # Area General Properties -[⬅ Area Configuration](../02-areas.md) - ## Introduction This documentation is dedicated to configuring the general properties of an area in the Antares Web application. @@ -18,6 +16,11 @@ To access the configuration of an area: ![01-properties-form.png](../../../assets/media/user-guide/study/areas/01-properties-form.png) -The area properties form allows you to configure the following elements: +The area properties form allows you to configure the following elements for each area of your study : + +- Energy cost +- Last resort shedding status +- Adequacy patch +- Output print status -> TODO +[⬅ Back to Area Configuration](../02-areas.md) diff --git a/docs/user-guide/study/areas/02-load.md b/docs/user-guide/study/areas/02-load.md index c2bf484dde..b1d7d06506 100644 --- a/docs/user-guide/study/areas/02-load.md +++ b/docs/user-guide/study/areas/02-load.md @@ -1,7 +1,5 @@ # Load Configuration -[⬅ Area Configuration](../02-areas.md) - ## Introduction This documentation is dedicated to configuring Load in the Antares Web application. @@ -24,12 +22,14 @@ and 1 column for each Monte-Carlo year. The available commands are: - **Assign a Matrix:** Search and assign a matrix from the matrix store to the load. -- **Import:** Drag and drop a TSV file to update the time series matrices. -- **Export:** Download the current TSV file using the "Export" button. +- **Import:** Drag and drop a TSV or CSV file to update the time series matrices. +- **Export:** Download the current TSV file using the "Export" button. You can also download this file in Excel format. You can edit a cell and confirm with the "Enter" key. You can also edit a group of cells or an entire column and confirm with the "Ctrl+Enter" key combination. -The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/latest/reference-guide/04-active_windows/#load). +The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/stable/user-guide/solver/02-inputs/#load). Explore these features to customize the ST Storage settings according to the specific needs of your study. + +[⬅ Back to Area Configuration](../02-areas.md) diff --git a/docs/user-guide/study/areas/03-thermals.md b/docs/user-guide/study/areas/03-thermals.md index 7e1d3f29ad..3b1467c879 100644 --- a/docs/user-guide/study/areas/03-thermals.md +++ b/docs/user-guide/study/areas/03-thermals.md @@ -1,7 +1,5 @@ # Thermal Clusters Configuration -[⬅ Area Configuration](../02-areas.md) - ## Introduction This documentation is dedicated to configuring Thermal Clusters (Thermals) in the Antares Web application. @@ -10,7 +8,7 @@ To access the configuration of Thermals: 1. From the "Study" view, click on the "MODELIZATION" tab. 2. Click on the "AREAS" tab, then choose an area from the sidebar. -3. Next, click on the "THERMALS" tab to access the page dedicated to Thermals. +3. Next, click on the "THERMAL" tab to access the page dedicated to Thermals. ![03-thermals.tab.png](../../../assets/media/user-guide/study/areas/03-thermals.tab.png) @@ -20,7 +18,7 @@ To access the configuration of Thermals: On the Thermals page, you will find the following elements: -- **Command Bar:** Add, duplicate, or delete thermal clusters using the "Add," "Duplicate," and "Delete" buttons. +- **Command Bar:** Add, duplicate, or delete thermal clusters using the "ADD," "DUPLICATE," and "DELETE" buttons. - **Toolbar:** Use the toolbar to filter and quickly search in the thermal clusters table. - **Selection and Actions:** Click on a row to select a thermal cluster. You can then delete or duplicate it. @@ -42,7 +40,7 @@ The **Total** row displays the sum of the values in the **Unit Count** and **Ena Click on the name of a thermal cluster to open the properties form. -![03-thermals.form.png](../../../assets/media/user-guide/study/areas/03-thermals.form.png) +![03-thermals.form.png](../../../assets/media/user-guide/study/areas/03-thermals-form.png) You will find the following elements: @@ -53,16 +51,23 @@ You will find the following elements: ## Time Series Matrices -In the tabs, you will find time series matrices composed of 8760 rows (hourly for a simulation year). +You will find at the bottom of the page differents tabs for matrix : +- COMMON : used to define the cluster's technico-economic characteristics +- TS GENERATOR : used to set the parameters of the stochastic generator +- AVAILABILITY : displays the "ready-made" 8760-hour time-series available for simulation purposes +- FUEL COSTS : used in case of TS Cost have the value "Use cost Timeseries" +- CO2 COSTS : used in case of TS Cost have the value "Use cost Timeseries" -![03-thermals.series.png](../../../assets/media/user-guide/study/areas/03-thermals.series.png) +![03-thermals.series.png](../../../assets/media/user-guide/study/areas/03-thermals-series.png) The available commands are: -- **Assign a Matrix:** Search and assign a matrix from the matrix store to Thermal Clusters. -- **Import:** Drag and drop a TSV file to update the time series matrices. -- **Export:** Download the current TSV file using the "Export" button. +- **IMPORT > From database:** Search and assign a matrix from the matrix store to Thermal Clusters. +- **IMPORT > From a file:** Drag and drop a TSV or CSV file to update the time series matrices. +- **Export:** Download the current TSV file using the "Export" button. You can also download the file in Excel format, choose this in the button dropdown list. You can edit a cell and confirm with the "Enter" key. You can also edit a group of cells or an entire column and confirm with the "Ctrl+Enter" key combination. -The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/latest/reference-guide/04-active_windows/#thermal). +The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/stable/user-guide/solver/02-inputs/#thermal). + +[⬅ Back to Area Configuration](../02-areas.md) \ No newline at end of file diff --git a/docs/user-guide/study/areas/04-renewables.md b/docs/user-guide/study/areas/04-renewables.md index f7298783c0..e079c7e719 100644 --- a/docs/user-guide/study/areas/04-renewables.md +++ b/docs/user-guide/study/areas/04-renewables.md @@ -1,7 +1,5 @@ # Renewable Clusters Configuration -[⬅ Area Configuration](../02-areas.md) - ## Introduction This documentation is dedicated to configuring Renewable Clusters (Renewables) in the Antares Web application. @@ -14,6 +12,12 @@ To access the configuration of Renewables: ![04-renewables.tab.png](../../../assets/media/user-guide/study/areas/04-renewables.tab.png) + +This tab is visible only when the study configuration have the value "Clusters" on Advanced parameters > Renewable generation modeling. + +![04-renewables-activation.png](../../../assets/media/user-guide/study/areas/04-renewables-activation.png) + + ## Renewable List ![04-renewables.list.png](../../../assets/media/user-guide/study/areas/04-renewables.list.png) @@ -58,10 +62,12 @@ In the tabs, you will find time series matrices composed of 8760 rows (hourly fo The available commands are: -- **Assign a Matrix:** Search and assign a matrix from the matrix store to Renewable Clusters. -- **Import:** Drag and drop a TSV file to update the time series matrices. -- **Export:** Download the current TSV file using the "Export" button. +- **IMPORT > From database:** Search and assign a matrix from the matrix store to Thermal Clusters. +- **IMPORT > From a file:** Drag and drop a TSV or CSV file to update the time series matrices. +- **Export:** Download the current TSV file using the "Export" button. You can also download the file in Excel format, choose this in the button dropdown list. You can edit a cell and confirm with the "Enter" key. You can also edit a group of cells or an entire column and confirm with the "Ctrl+Enter" key combination. -The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/latest/reference-guide/04-active_windows/#renewable). +The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/stable/user-guide/solver/02-inputs/#renewable). + +[⬅ Back to Area Configuration](../02-areas.md) \ No newline at end of file diff --git a/docs/user-guide/study/areas/05-hydro.md b/docs/user-guide/study/areas/05-hydro.md index c2a535bb2c..6e664584d0 100644 --- a/docs/user-guide/study/areas/05-hydro.md +++ b/docs/user-guide/study/areas/05-hydro.md @@ -1,6 +1,25 @@ +--- +title: Hydraulic Generators Configuration +author: Antares Web Team +date: 2024-11-22 +category: User Guide +tags: + + - hydraulic generators configuration + - hydro + - inflow structure + - allocation + - correlation + - daily power & energy credits + - reservoir levels + - water values + - hydro storage + - ROR run of river + - min gen + +--- # Hydraulic Generators Configuration - -[⬅ Area Configuration](../02-areas.md) +**_This section is under construction_** ## Introduction @@ -22,33 +41,27 @@ This tab allows you to configure the management options of the hydraulic generat ## Inflow Structure -This tab allows you to configure the inflow pattern time series and the overall monthly hydro parameters. +This tab allows you to configure the inter-monthly correlation, inflow pattern time series and the overall monthly hydro parameters. -> TODO +![05-hydro-inflowstructure.png](../../../assets/media/user-guide/study/areas/05-hydro-inflowstructure.png) ## Allocation This tab allows you to configure the allocation coefficients for each area. -> TODO +![05-hydro-allocation.png](../../../assets/media/user-guide/study/areas/05-hydro-allocation.png) ## Correlation This tab allows you to configure the correlation coefficients between the current area and the other areas. -> TODO - -## Daily Power +![05-hydro-correlation.png](../../../assets/media/user-guide/study/areas/05-hydro-correlation.png) -This tab allows you to configure the daily generating power and the pumping power of the hydraulic generators. +## Daily Power & Energy Credits -> TODO - -## Energy Credits +This tab allows you to configure the daily generating power and the pumping power of the hydraulic generators, the energy credits time series of the hydraulic generators. -This tab allows you to configure the energy credits time series of the hydraulic generators. - -> TODO +![05-hydro-dailypower-energycredits.png](../../../assets/media/user-guide/study/areas/05-hydro-dailypower-energycredits.png) ## Reservoir Levels @@ -76,4 +89,6 @@ This tab allows you to configure the run of river time series of the hydraulic g The "Min Gen." tab is dedicated to configuring the minimum generation levels of the hydraulic generators. This tab presents a time series that represents the minimum hourly production for one or more Monte-Carlo years. -![05-hydro.min-generation.series.png](../../../assets/media/user-guide/study/areas/05-hydro.min-generation.series.png) \ No newline at end of file +![05-hydro.min-generation.series.png](../../../assets/media/user-guide/study/areas/05-hydro.min-generation.series.png) + +[⬅ Back to Area Configuration](../02-areas.md) \ No newline at end of file diff --git a/docs/user-guide/study/areas/06-wind.md b/docs/user-guide/study/areas/06-wind.md index 176ea212de..884bfcdcb0 100644 --- a/docs/user-guide/study/areas/06-wind.md +++ b/docs/user-guide/study/areas/06-wind.md @@ -13,3 +13,9 @@ To access the configuration of Wind: 3. Next, click on the "WIND" tab to access the page dedicated to wind generators. ![06-wind.tab.png](../../../assets/media/user-guide/study/areas/06-wind.tab.png) + +You will see here 8760-hour time-series already available for simulation purposes. You can update this data. + +This tab is visible only when the study configuration have the value "Aggregated" on Advanced parameters > Renewable generation modeling. + +![06-wind-activation.png](../../../assets/media/user-guide/study/areas/06-wind-activation.png) diff --git a/docs/user-guide/study/areas/08-st-storages.md b/docs/user-guide/study/areas/08-st-storages.md index 11f1135a74..881dd9ebfc 100644 --- a/docs/user-guide/study/areas/08-st-storages.md +++ b/docs/user-guide/study/areas/08-st-storages.md @@ -1,7 +1,18 @@ +--- +title: Short-Term Storage Configuration +author: Antares Web Team +date: 2024-11-22 +category: User Guide +tags: + + - short-term storage configuration + - injection capacity + - withdrawal capacity + - stock + +--- # Short-Term Storage Configuration -[⬅ Area Configuration](../02-areas.md) - ## Introduction This documentation is dedicated to configuring short-term storage (ST Storage) in the Antares Web application. @@ -17,7 +28,11 @@ To access the configuration of ST storages: ## ST Storage List -![08-st-storages.list.png](../../../assets/media/user-guide/study/areas/08-st-storages.list.png) +![08-st-storages-list.png](../../../assets/media/user-guide/study/areas/08-st-storage-list.png) +_Screenshot for a study version under 8.8_ + +![08-st-storages-list-enable.png](../../../assets/media/user-guide/study/areas/08-st-storage-list-enable.png) +_Screenshot for a study version in 8.8_ On the ST storages page, you will find the following elements: @@ -29,21 +44,26 @@ The storages table displays the following information: - **Group:** Name of the group to which the storage belongs. - **Name:** Name of the storage (link to the properties form). -- **Withdrawal (MW):** Withdrawal power of the storage. -- **Injection (MW):** Injection power of the storage. -- **Reservoir (MWh):** Reservoir capacity of the storage. +- **Enabled:** Indicates whether the storage is enable or disable. Only from version 8.8 of studies. +- **Stored (MW):** Withdrawal power of the storage. +- **Released (MW):** Injection power of the storage. +- **Stock (MWh):** Reservoir capacity of the storage. - **Efficiency (%):** Efficiency of the storage. - **Initial Level (%):** Initial level of the storage. - **Initial Level Optimized:** Indicates whether the initial level of the storage is optimized. -The **Total** row displays the sum of the values in the **Withdrawal** and **Injection** columns. +The **Total** row displays the sum of the values in the **Stored** and **Released** columns. +### New update +From version 8.8 of the studies, a new parameter has been added to the short-term storage objects. This is an option that allows you to enable/disable storage. +Note that this option is not valid for studies lower than version 8.8. To be able to use it, you must update your study to at least version 8.8. ## Configuration Form Click on the name of a storage to open the properties form. -![08-st-storages.form.png](../../../assets/media/user-guide/study/areas/08-st-storages.form.png) +![08-st-storages-form.png](../../../assets/media/user-guide/study/areas/08-st-storages-form.png) +_Screenshot for a study version under 8.8_ You will find the following elements: @@ -51,20 +71,20 @@ You will find the following elements: - Modify the values and click "Save" to confirm the changes. - Use the "↶" buttons to undo changes and "↷" to redo them, confirm the modification with "Save." -The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/latest/reference-guide/13-file-format/#short-term-storage_1). - ## Time Series Matrices In the tabs, you will find time series matrices composed of 8760 rows (hourly for a simulation year). -![08-st-storages.series.png](../../../assets/media/user-guide/study/areas/08-st-storages.series.png) +![08-st-storages-series.png](../../../assets/media/user-guide/study/areas/08-st-storages-series.png) The available commands are: -- **Assign a Matrix:** Search and assign a matrix from the matrix store to short-term storage. -- **Import:** Drag and drop a TSV file to update the time series matrices. -- **Export:** Download the current TSV file using the "Export" button. +- **IMPORT > From database:** Search and assign a matrix from the matrix store to Thermal Clusters. +- **IMPORT > From a file:** Drag and drop a TSV or CSV file to update the time series matrices. +- **Export:** Download the current TSV file using the "Export" button. You can also download the file in Excel format, choose this in the button dropdown list. You can edit a cell and confirm with the "Enter" key. You can also edit a group of cells or an entire column and confirm with the "Ctrl+Enter" key combination. -The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/latest/reference-guide/13-file-format/#short-term-storage_1). +The detailed configuration is available in the [Antares Simulator documentation](https://antares-simulator.readthedocs.io/en/stable/user-guide/solver/02-inputs/#storages). + +[⬅ Back to Area Configuration](../02-areas.md) diff --git a/mkdocs.yml b/mkdocs.yml index 5a28ea96df..02bd594987 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,12 +32,11 @@ nav: - 'User guide': - 'Introduction': 'user-guide/0-introduction.md' - 'User interface': 'user-guide/1-interface.md' - - 'Study Configuration': - - 'Main Topics': 'user-guide/2-study.md' - - 'Map': - - 'Main Topics': 'user-guide/study/01-map.md' + - 'Study Modelization': + - 'Overview': 'user-guide/2-study.md' + - 'Map': 'user-guide/study/01-map.md' - 'Area Configuration': - - 'Main Topics': 'user-guide/study/02-areas.md' + - 'Overview': 'user-guide/study/02-areas.md' - 'General Properties': 'user-guide/study/areas/01-properties.md' - 'Load': 'user-guide/study/areas/02-load.md' - 'Thermal Clusters': 'user-guide/study/areas/03-thermals.md' @@ -48,30 +47,41 @@ nav: - 'Short-Term Storages': 'user-guide/study/areas/08-st-storages.md' - 'Reserves': 'user-guide/study/areas/09-reserves.md' - 'Miscellaneous Generators': 'user-guide/study/areas/10-misc-gen.md' - - 'Links': - - 'Main Topics': 'user-guide/study/03-links.md' - - 'Binding Constraints': - - 'Main Topics': 'user-guide/study/04-binding-constraints.md' - - 'Debug': - - 'Main Topics': 'user-guide/study/05-debug.md' - - 'Table Mode': - - 'Main Topics': 'user-guide/study/06-table-mode.md' + - 'Links': 'user-guide/study/03-links.md' + - 'Binding Constraints': 'user-guide/study/04-binding-constraints.md' + - 'Simulation Configuration': + 'General Configuration': 'user-guide/simulation-configuration/all-configurations.md#general-configuration' + 'TS management': 'user-guide/simulation-configuration/all-configurations.md#Time-series management' + 'Optimization preferences': 'user-guide/simulation-configuration/all-configurations.md#optimization-preferences' + 'Adequacy Patch': 'user-guide/simulation-configuration/all-configurations.md#adequacy-patch' + 'Advanced parameters': 'user-guide/simulation-configuration/all-configurations.md#advanced-parameters' + 'Economic Opt.': 'user-guide/simulation-configuration/all-configurations.md#economic-opt' + 'Geographic Trimming (Areas)': 'user-guide/simulation-configuration/all-configurations.md#geographic-trimming(areas)' + 'Geographic Trimming (Links)': 'user-guide/simulation-configuration/all-configurations.md#geographic-trimming(links)' + 'Geographic Trimming (Binding Constraints)': 'user-guide/simulation-configuration/all-configurations.md#geographic-trimming(binding-constrainte)' + - 'Table Mode': 'user-guide/study/06-table-mode.md' + - 'Xpansion' : user-guide/study/07-xpansion.md + - 'Results' : user-guide/study/08-results.md + - 'Debug': 'user-guide/study/05-debug.md' - 'Variant manager': 'user-guide/3-variant_manager.md' - - 'How to': - - 'Create a study': 'how-to/studies-create.md' - - 'Import a study': 'how-to/studies-import.md' - - 'Upgrade a study': 'how-to/studies-upgrade.md' - - 'Build': - - 'Introduction': 'install/0-INSTALL.md' - - 'Configuration': 'install/1-CONFIG.md' - - 'Deployment': 'install/2-DEPLOY.md' - - 'Develop': - - 'Introduction': 'architecture/0-introduction.md' - - 'Database management': 'architecture/1-database.md' - - 'Add a new Study version': 'architecture/2-add-new-antares-version.md' - - 'Roadmap': 'architecture/5-roadmap.md' - - 'Antares ecosystem': 'https://antares-doc.readthedocs.io' - - 'Changelog': 'CHANGELOG.md' + - 'How to': + - 'Create a study': 'user-guide/how-to/studies-create.md' + - 'Import a study': 'user-guide/how-to/studies-import.md' + - 'Upgrade a study': 'user-guide/how-to/studies-upgrade.md' + - 'Developer guide': + - 'Build': + - 'Introduction': 'developer-guide/install/0-INSTALL.md' + - 'Configuration': 'developer-guide/install/1-CONFIG.md' + - 'Deployment': 'developer-guide/install/2-DEPLOY.md' + - 'Architecture': 'developer-guide/architecture/0-introduction.md' + - 'Database management': 'developer-guide/architecture/1-database.md' + - 'Add a new study version': 'developer-guide/2-add-new-antares-version.md' + - 'Changelog': 'CHANGELOG.md' + - 'Roadmap': 'developer-guide/5-roadmap.md' + - 'External links' : + - 'Antares ecosystem': 'https://antares-doc.readthedocs.io' + - 'Antares website' : 'https://antares-simulator.org/' + - 'RTE website' : 'http://www.rte-france.com/' extra: generator: false @@ -101,4 +111,4 @@ markdown_extensions: - pymdownx.superfences - pymdownx.tabbed -copyright: Copyright © 2007 - 2023 RTE \ No newline at end of file +copyright: Copyright © 2007 - 2024 RTE \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cfde64c238..5e77bb439e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools"] [project] name = "AntaREST" -version = "2.17.6" +version = "2.18.0" authors = [{name="RTE, Antares Web Team", email="andrea.sgattoni@rte-france.com" }] description="Antares Server" readme = {file = "README.md", content-type = "text/markdown"} diff --git a/resources/deploy/README.md b/resources/antares-desktop-fs/README.md similarity index 88% rename from resources/deploy/README.md rename to resources/antares-desktop-fs/README.md index 138c2dba73..1562196e02 100644 --- a/resources/deploy/README.md +++ b/resources/antares-desktop-fs/README.md @@ -30,7 +30,8 @@ Run the following command to launch the server: ## Accessing the Web Server -Once the Antares Web server is running, you can access it using a web browser. +Once the Antares Web server is running, the default web browser will automatically open to the application home page. +You can also manually access by following these steps: 1. Open a web browser. 2. Enter the following URL in the address bar: @@ -56,4 +57,4 @@ The tool has the following subcommands: - `generate-script-diff`: Generate variant script commands from two variant script directories - `upgrade-study`: Upgrades study version -Further instructions can be found in the online help. Use the `--help' option. +Further instructions can be found in the online help. Use the `--help` option. diff --git a/resources/antares-desktop-fs/archives/.placeholder b/resources/antares-desktop-fs/archives/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/antares-desktop-fs/config.yaml b/resources/antares-desktop-fs/config.yaml new file mode 100644 index 0000000000..c151490a47 --- /dev/null +++ b/resources/antares-desktop-fs/config.yaml @@ -0,0 +1,58 @@ +# Documentation about this file can be found in this file: `docs/install/1-CONFIG.md` + +security: + disabled: true + jwt: + key: super-secret + +db: + url: "sqlite:///database.db" + +storage: + tmp_dir: ./tmp + matrixstore: ./matrices + archive_dir: ./archives + workspaces: + default: + path: ./internal_studies/ + studies: + path: ./studies/ + +launcher: + local: + binaries: + VER: ANTARES_SOLVER_PATH + +# slurm: +# local_workspace: /path/to/slurm_workspace # Path to the local SLURM workspace +# username: run-antares # SLURM username +# hostname: 10.134.248.111 # SLURM server hostname +# port: 22 # SSH port for SLURM +# private_key_file: /path/to/ssh_private_key # SSH private key file +# default_wait_time: 900 # Default wait time for SLURM jobs +# default_time_limit: 172800 # Default time limit for SLURM jobs +# enable_nb_cores_detection: False # Enable detection of available CPU cores for SLURM +# nb_cores: +# min: 1 # Minimum number of CPU cores +# default: 22 # Default number of CPU cores +# max: 24 # Maximum number of CPU cores +# default_json_db_name: launcher_db.json # Default JSON database name for SLURM +# slurm_script_path: /applis/antares/launchAntares.sh # Path to the SLURM script (on distant server) +# db_primary_key: name # Primary key for the SLURM database +# antares_versions_on_remote_server: #List of Antares versions available on the remote SLURM server +# - "840" +# - "850" + + +debug: false + +# Serve the API at /api +api_prefix: "/api" + +server: + worker_threadpool_size: 12 + services: + - watcher + +logging: + logfile: ./logs/antarest.log diff --git a/resources/antares-desktop-fs/internal_studies/.placeholder b/resources/antares-desktop-fs/internal_studies/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/antares-desktop-fs/logs/.placeholder b/resources/antares-desktop-fs/logs/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/antares-desktop-fs/matrices/.placeholder b/resources/antares-desktop-fs/matrices/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/antares-desktop-fs/studies/.placeholder b/resources/antares-desktop-fs/studies/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/antares-desktop-fs/studies/README.md b/resources/antares-desktop-fs/studies/README.md new file mode 100644 index 0000000000..3af92813f8 --- /dev/null +++ b/resources/antares-desktop-fs/studies/README.md @@ -0,0 +1,2 @@ +Examples can be found at https://github.com/AntaresSimulatorTeam/Antares_Simulator_Examples +These can be copied into the `studies` directory. diff --git a/resources/antares-desktop-fs/tmp/.placeholder b/resources/antares-desktop-fs/tmp/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/deploy/examples/.placeholder b/resources/deploy/examples/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/package_antares_web.sh b/scripts/package_antares_web.sh index 9a0efb5e15..9743fbc049 100755 --- a/scripts/package_antares_web.sh +++ b/scripts/package_antares_web.sh @@ -74,7 +74,7 @@ fi echo "INFO: Copying basic configuration files..." rm -rf "${DIST_DIR}/examples" # in case of replay -cp -r "${RESOURCES_DIR}"/deploy/* "${DIST_DIR}" +cp -r "${RESOURCES_DIR}"/antares-desktop-fs/* "${DIST_DIR}" if [[ "$OSTYPE" == "msys"* ]]; then sed -i "s/VER: ANTARES_SOLVER_PATH/$ANTARES_SOLVER_VERSION_INT: .\/AntaresWeb\/antares_solver\/antares-$ANTARES_SOLVER_VERSION-solver.exe/g" "${DIST_DIR}/config.yaml" else @@ -92,7 +92,9 @@ else fi echo "INFO: Unzipping example study..." -cd "${DIST_DIR}/examples/studies" || exit +# Basic study is located in the `deploy` directory +cp -r "${RESOURCES_DIR}/deploy/examples/studies/"* "${DIST_DIR}/studies" +cd "${DIST_DIR}/studies" || exit if [[ "$OSTYPE" == "msys"* ]]; then 7z x example_study.zip else diff --git a/sonar-project.properties b/sonar-project.properties index dd7384df7d..c0897c1d14 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,5 +6,5 @@ sonar.exclusions=antarest/gui.py,antarest/main.py sonar.python.coverage.reportPaths=coverage.xml sonar.python.version=3.11 sonar.javascript.lcov.reportPaths=webapp/coverage/lcov.info -sonar.projectVersion=2.17.6 +sonar.projectVersion=2.18.0 sonar.coverage.exclusions=antarest/gui.py,antarest/main.py,antarest/singleton_services.py,antarest/worker/archive_worker_service.py,webapp/**/*,,antarest/fastapi_jwt_auth/** \ No newline at end of file diff --git a/tests/integration/prepare_proxy.py b/tests/integration/prepare_proxy.py index 47e5a33294..b1fa804a1f 100644 --- a/tests/integration/prepare_proxy.py +++ b/tests/integration/prepare_proxy.py @@ -109,9 +109,9 @@ def upload_matrix(self, study_id: str, matrix_path: str, df: pd.DataFrame) -> No # noinspection SpellCheckingInspection res = self.client.put( f"/v1/studies/{study_id}/raw", - params={"path": matrix_path, "create_missing": True}, # type: ignore + params={"path": matrix_path}, # type: ignore headers=self.headers, - files={"file": tsv, "create_missing": "true"}, # type: ignore + files={"file": tsv}, # type: ignore ) res.raise_for_status() diff --git a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py index da4426bdb2..58749c0b09 100644 --- a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py +++ b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py @@ -21,26 +21,48 @@ import numpy as np import pandas as pd import pytest +from httpx import Response from starlette.testclient import TestClient +from antarest.core.tasks.model import TaskStatus from antarest.core.utils.fastapi_sqlalchemy import db from antarest.study.model import RawStudy, Study from tests.integration.raw_studies_blueprint.assets import ASSETS_DIR from tests.integration.utils import wait_for +def _check_endpoint_response( + study_type: str, res: Response, client: TestClient, study_id: str, expected_msg: str, exception: str +): + # The command will only fail when applied so on raw studies only. + # So we have to differentiate the test based on the study type. + if study_type == "raw": + assert res.status_code == 403 + assert res.json()["exception"] == exception + assert expected_msg in res.json()["description"] + else: + res.raise_for_status() + task_id = client.put(f"/v1/studies/{study_id}/generate").json() + res = client.get(f"/v1/tasks/{task_id}?wait_for_completion=True") + task = res.json() + assert task["status"] == TaskStatus.FAILED.value + assert not task["result"]["success"] + assert expected_msg in task["result"]["message"] + # We have to delete the command to make the variant "clean" again. + res = client.get(f"/v1/studies/{study_id}/commands") + cmd_id = res.json()[-1]["id"] + res = client.delete(f"/v1/studies/{study_id}/commands/{cmd_id}") + res.raise_for_status() + + @pytest.mark.integration_test class TestFetchRawData: """ Check the retrieval of Raw Data from Study: JSON, Text, or File Attachment. """ - def test_get_study( - self, - client: TestClient, - user_access_token: str, - internal_study_id: str, - ): + @pytest.mark.parametrize("study_type", ["raw", "variant"]) + def test_get_study(self, client: TestClient, user_access_token: str, internal_study_id: str, study_type: str): """ Test the `get_study` endpoint for fetching raw data from a study. @@ -54,12 +76,16 @@ def test_get_study( 4. Uses the API to download files from the "user/unknown" directory. 5. Checks for a 415 error when the extension of a file is unknown. """ + + # ============================= + # SET UP + # ============================= + # First copy the user resources in the Study directory with db(): study: RawStudy = db.session.get(Study, internal_study_id) study_dir = pathlib.Path(study.path) client.headers = {"Authorization": f"Bearer {user_access_token}"} - raw_url = f"/v1/studies/{internal_study_id}/raw" shutil.copytree( ASSETS_DIR.joinpath("user"), @@ -67,6 +93,24 @@ def test_get_study( dirs_exist_ok=True, ) + if study_type == "variant": + # Copies the study, to convert it into a managed one. + res = client.post( + f"/v1/studies/{internal_study_id}/copy", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"dest": "default", "with_outputs": False, "use_task": False}, + ) + assert res.status_code == 201 + parent_id = res.json() + res = client.post(f"/v1/studies/{parent_id}/variants", params={"name": "variant 1"}) + internal_study_id = res.json() + + raw_url = f"/v1/studies/{internal_study_id}/raw" + + # ============================= + # NOMINAL CASES + # ============================= + # Then, use the API to download the files from the "user/folder" directory user_folder_dir = study_dir.joinpath("user/folder") for file_path in user_folder_dir.glob("*.*"): @@ -120,24 +164,40 @@ def test_get_study( # To create a resource, you can use PUT method and the `create_missing` flag. # The expected status code should be 204 No Content. + file_to_create = "user/somewhere/something.txt" res = client.put( raw_url, - params={"path": "user/somewhere/something.txt", "create_missing": True}, + params={"path": file_to_create, "create_missing": True}, files={"file": io.BytesIO(b"Goodbye Cruel World!")}, ) assert res.status_code == 204, res.json() + if study_type == "variant": + # Asserts the generation succeeds + task_id = client.put(f"/v1/studies/{internal_study_id}/generate?from_scratch=True").json() + res = client.get(f"/v1/tasks/{task_id}?wait_for_completion=True") + task = res.json() + assert task["status"] == TaskStatus.COMPLETED.value + assert task["result"]["success"] + # Checks created commands + res = client.get(f"/v1/studies/{internal_study_id}/commands") + commands = res.json() + # First command is created automatically to respect owners, we ignore it. + assert commands[1]["action"] == "create_user_resource" + assert commands[1]["args"] == [{"data": {"path": "somewhere/something.txt", "resource_type": "file"}}] + assert commands[2]["action"] == "update_file" + assert commands[2]["args"] == [{"target": file_to_create, "b64Data": "R29vZGJ5ZSBDcnVlbCBXb3JsZCE="}] # To update a resource, you can use PUT method, with or without the `create_missing` flag. # The expected status code should be 204 No Content. res = client.put( raw_url, - params={"path": "user/somewhere/something.txt", "create_missing": True}, + params={"path": file_to_create, "create_missing": True}, files={"file": io.BytesIO(b"This is the end!")}, ) assert res.status_code == 204, res.json() # You can check that the resource has been created or updated. - res = client.get(raw_url, params={"path": "user/somewhere/something.txt"}) + res = client.get(raw_url, params={"path": file_to_create}) assert res.status_code == 200, res.json() assert res.content == b"This is the end!" @@ -161,7 +221,7 @@ def test_get_study( written_data = res.json()["data"] if not content.decode("utf-8"): # For some reason the `GET` returns the default matrix when it's empty - expected = 8760 * [[0]] + expected = 8760 * [[0]] if study_type == "raw" else [[]] else: df = pd.read_csv(io.BytesIO(content), delimiter=delimiter, header=None).replace(",", ".", regex=True) expected = df.to_numpy(dtype=np.float64).tolist() @@ -196,7 +256,6 @@ def test_get_study( assert actual == {"index": ANY, "columns": ANY, "data": ANY} # If we ask for a matrix, we should have a CSV content if formatted is False - rel_path = "/input/links/de/fr" res = client.get(raw_url, params={"path": rel_path, "formatted": False}) assert res.status_code == 200, res.json() actual = res.text @@ -228,9 +287,10 @@ def test_get_study( assert res.json() == ["DE", "ES", "FR", "IT"] # asserts that the GET /raw endpoint is able to read matrix containing NaN values - res = client.get(raw_url, params={"path": "output/20201014-1427eco/economy/mc-all/areas/de/id-monthly"}) - assert res.status_code == 200 - assert np.isnan(res.json()["data"][0]).any() + if study_type == "raw": + res = client.get(raw_url, params={"path": "output/20201014-1427eco/economy/mc-all/areas/de/id-monthly"}) + assert res.status_code == 200 + assert np.isnan(res.json()["data"][0]).any() # Iterate over all possible combinations of path and depth for path, depth in itertools.product([None, "", "/"], [0, 1, 2]): @@ -238,11 +298,27 @@ def test_get_study( assert res.status_code == 200, f"Error for path={path} and depth={depth}" -def test_delete_raw(client: TestClient, user_access_token: str, internal_study_id: str) -> None: +@pytest.mark.parametrize("study_type", ["raw", "variant"]) +def test_delete_raw(client: TestClient, user_access_token: str, internal_study_id: str, study_type: str) -> None: + # ============================= + # SET UP + # ============================= client.headers = {"Authorization": f"Bearer {user_access_token}"} + if study_type == "variant": + # Copies the study, to convert it into a managed one. + res = client.post( + f"/v1/studies/{internal_study_id}/copy", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"dest": "default", "with_outputs": False, "use_task": False}, + ) + assert res.status_code == 201 + parent_id = res.json() + res = client.post(f"/v1/studies/{parent_id}/variants", params={"name": "variant 1"}) + internal_study_id = res.json() + # ============================= - # SET UP + NOMINAL CASES + # NOMINAL CASES # ============================= content = io.BytesIO(b"This is the end!") @@ -284,21 +360,96 @@ def test_delete_raw(client: TestClient, user_access_token: str, internal_study_i # try to delete expansion folder res = client.delete(f"/v1/studies/{internal_study_id}/raw?path=/user/expansion") - assert res.status_code == 403 - assert res.json()["exception"] == "FileDeletionNotAllowed" - assert "you are not allowed to delete this resource" in res.json()["description"] + expected_msg = "you are not allowed to delete this resource" + _check_endpoint_response(study_type, res, client, internal_study_id, expected_msg, "ResourceDeletionNotAllowed") # try to delete a file which isn't inside the 'User' folder res = client.delete(f"/v1/studies/{internal_study_id}/raw?path=/input/thermal") + expected_msg = "the given path isn't inside the 'User' folder" assert res.status_code == 403 - assert res.json()["exception"] == "FileDeletionNotAllowed" - assert "the targeted data isn't inside the 'User' folder" in res.json()["description"] + assert res.json()["exception"] == "ResourceDeletionNotAllowed" + assert expected_msg in res.json()["description"] # With a path that doesn't exist res = client.delete(f"/v1/studies/{internal_study_id}/raw?path=user/fake_folder/fake_file.txt") + expected_msg = "the given path doesn't exist" + _check_endpoint_response(study_type, res, client, internal_study_id, expected_msg, "ResourceDeletionNotAllowed") + + +@pytest.mark.parametrize("study_type", ["raw", "variant"]) +def test_create_folder(client: TestClient, user_access_token: str, internal_study_id: str, study_type: str) -> None: + client.headers = {"Authorization": f"Bearer {user_access_token}"} + + if study_type == "variant": + # Copies the study, to convert it into a managed one. + res = client.post( + f"/v1/studies/{internal_study_id}/copy", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"dest": "default", "with_outputs": False, "use_task": False}, + ) + assert res.status_code == 201 + parent_id = res.json() + res = client.post(f"/v1/studies/{parent_id}/variants", params={"name": "variant 1"}) + internal_study_id = res.json() + + raw_url = f"/v1/studies/{internal_study_id}/raw" + + # ============================= + # NOMINAL CASES + # ============================= + additional_params = {"resource_type": "folder", "create_missing": True} + + res = client.put(raw_url, params={"path": "user/folder_1", **additional_params}) + assert res.status_code == 204 + + # same case with different writing should succeed + res = client.put(raw_url, params={"path": "/user/folder_2", **additional_params}) + assert res.status_code == 204 + + # create a folder within a non-existing one + res = client.put(raw_url, params={"path": "/user/folder_x/folder_y", **additional_params}) + assert res.status_code == 204 + + # checks debug view to see that folders were created + res = client.get(f"/v1/studies/{internal_study_id}/raw?path=&depth=-1") + assert res.status_code == 200 + tree = res.json()["user"] + assert list(tree.keys()) == ["expansion", "folder_1", "folder_2", "folder_x"] + assert tree["folder_x"] == {"folder_y": {}} + + # ============================= + # ERRORS + # ============================= + + # we can't create a file without specifying a content + res = client.put(raw_url, params={"path": "fake_path"}) + assert res.status_code == 422 + assert res.json()["description"] == "Argument mismatch: Must give a content to create a file" + + # we can't create a folder and specify a content at the same time + res = client.put(raw_url, params={"path": "", "resource_type": "folder"}, files={"file": b"content"}) + assert res.status_code == 422 + assert res.json()["description"] == "Argument mismatch: Cannot give a content to create a folder" + + # try to create a folder outside `user` folder + wrong_folder = "input/wrong_folder" + expected_msg = f"the given path isn't inside the 'User' folder: {wrong_folder}" + res = client.put(raw_url, params={"path": wrong_folder, **additional_params}) assert res.status_code == 403 - assert res.json()["exception"] == "FileDeletionNotAllowed" - assert "the given path doesn't exist" in res.json()["description"] + assert res.json()["exception"] == "FolderCreationNotAllowed" + assert expected_msg in res.json()["description"] + + # try to create a folder inside the 'expansion` folder + expansion_folder = "user/expansion/wrong_folder" + expected_msg = "you are not allowed to create a resource here" + res = client.put(raw_url, params={"path": expansion_folder, **additional_params}) + _check_endpoint_response(study_type, res, client, internal_study_id, expected_msg, "FolderCreationNotAllowed") + + # try to create an already existing folder + existing_folder = "user/folder_1" + expected_msg = "the given resource already exists" + res = client.put(raw_url, params={"path": existing_folder, **additional_params}) + _check_endpoint_response(study_type, res, client, internal_study_id, expected_msg, "FolderCreationNotAllowed") def test_retrieve_from_archive(client: TestClient, user_access_token: str) -> None: diff --git a/tests/integration/study_data_blueprint/test_generate_thermal_cluster_timeseries.py b/tests/integration/study_data_blueprint/test_generate_thermal_cluster_timeseries.py index 2a41307e06..ff8eda61e4 100644 --- a/tests/integration/study_data_blueprint/test_generate_thermal_cluster_timeseries.py +++ b/tests/integration/study_data_blueprint/test_generate_thermal_cluster_timeseries.py @@ -11,6 +11,7 @@ # This file is part of the Antares project. import numpy as np +import pytest from starlette.testclient import TestClient from antarest.core.tasks.model import TaskDTO, TaskStatus @@ -99,24 +100,18 @@ def test_lifecycle_nominal(self, client: TestClient, user_access_token: str) -> data = res.json()["data"] assert data == [[]] # no generation c.f. gen-ts parameter - def test_errors_and_limit_cases(self, client: TestClient, user_access_token: str) -> None: + @pytest.mark.parametrize("study_type", ["raw", "variant"]) + def test_errors_and_limit_cases(self, client: TestClient, user_access_token: str, study_type: str) -> None: # Study Preparation client.headers = {"Authorization": f"Bearer {user_access_token}"} preparer = PreparerProxy(client, user_access_token) study_id = preparer.create_study("foo", version=860) area1_id = preparer.create_area(study_id, name="Area 1")["id"] + if study_type == "variant": + study_id = preparer.create_variant(study_id, name="Variant 1") - # Create a cluster without nominal power cluster_name = "Cluster 1" preparer.create_thermal(study_id, area1_id, name=cluster_name, group="Lignite") - # Timeseries generation fails because there's no nominal power - task = self._generate_timeseries(client, user_access_token, study_id) - assert task.status == TaskStatus.FAILED - assert ( - f"Area {area1_id}, cluster {cluster_name.lower()}: Nominal power must be strictly positive, got 0.0" - in task.result.message - ) - # Puts the nominal power as a float body = {"nominalCapacity": 4.4} res = client.patch(f"/v1/studies/{study_id}/areas/{area1_id}/clusters/thermal/{cluster_name}", json=body) @@ -145,6 +140,18 @@ def test_errors_and_limit_cases(self, client: TestClient, user_access_token: str task = self._generate_timeseries(client, user_access_token, study_id) assert task.status == TaskStatus.COMPLETED + # Puts nominal capacity at 0 + body = {"nominalCapacity": 0} + res = client.patch(f"/v1/studies/{study_id}/areas/{area1_id}/clusters/thermal/{cluster_name}", json=body) + assert res.status_code in {200, 201} + # Timeseries generation fails because there's no nominal power + task = self._generate_timeseries(client, user_access_token, study_id) + assert task.status == TaskStatus.FAILED + assert ( + f"Area {area1_id}, cluster {cluster_name.lower()}: Nominal power must be strictly positive, got 0.0" + in task.result.message + ) + def test_advanced_results(self, client: TestClient, user_access_token: str) -> None: # Study Preparation client.headers = {"Authorization": f"Bearer {user_access_token}"} diff --git a/tests/integration/study_data_blueprint/test_link.py b/tests/integration/study_data_blueprint/test_link.py index 7e4d18bf2c..635aedb830 100644 --- a/tests/integration/study_data_blueprint/test_link.py +++ b/tests/integration/study_data_blueprint/test_link.py @@ -9,6 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +from sys import stderr import pytest from starlette.testclient import TestClient @@ -19,6 +20,102 @@ @pytest.mark.unit_test class TestLink: + @pytest.mark.parametrize("study_type", ["raw", "variant"]) + def test_link_update(self, client: TestClient, user_access_token: str, study_type: str) -> None: + client.headers = {"Authorization": f"Bearer {user_access_token}"} # type: ignore + + preparer = PreparerProxy(client, user_access_token) + study_id = preparer.create_study("foo", version=820) + if study_type == "variant": + study_id = preparer.create_variant(study_id, name="Variant 1") + + area1_id = preparer.create_area(study_id, name="Area 1")["id"] + area2_id = preparer.create_area(study_id, name="Area 2")["id"] + client.post(f"/v1/studies/{study_id}/links", json={"area1": area1_id, "area2": area2_id, "hurdlesCost": True}) + res = client.put( + f"/v1/studies/{study_id}/links/{area1_id}/{area2_id}", + json={"colorr": 150}, + ) + + assert res.status_code == 200 + expected = { + "area1": "area 1", + "area2": "area 2", + "assetType": "ac", + "colorb": 112, + "colorg": 112, + "colorr": 150, + "displayComments": True, + "filterSynthesis": "hourly, daily, weekly, monthly, annual", + "filterYearByYear": "hourly, daily, weekly, monthly, annual", + "hurdlesCost": True, + "linkStyle": "plain", + "linkWidth": 1.0, + "loopFlow": False, + "transmissionCapacities": "enabled", + "usePhaseShifter": False, + } + assert expected == res.json() + + # Test update link same area + + res = client.put( + f"/v1/studies/{study_id}/links/{area1_id}/{area1_id}", + json={"hurdlesCost": False}, + ) + assert res.status_code == 422 + expected = { + "description": "Cannot create a link that goes from and to the same single area: area 1", + "exception": "LinkValidationError", + } + assert expected == res.json() + + # Test update link with non existing area + + res = client.put( + f"/v1/studies/{study_id}/links/{area1_id}/id_do_not_exist", + json={"hurdlesCost": False}, + ) + assert res.status_code == 404 + expected = { + "description": "The link area 1 -> id_do_not_exist is not present in the study", + "exception": "LinkNotFound", + } + assert expected == res.json() + + # Test update link fails when given wrong parameters + if study_type == "raw": + res = client.post( + f"/v1/studies/{study_id}/commands", + json=[ + { + "action": "update_link", + "args": { + "area1": area1_id, + "area2": area2_id, + "parameters": {"hurdles-cost": False, "wrong": "parameter"}, + }, + } + ], + ) + assert res.status_code == 500 + expected = "Unexpected exception occurred when trying to apply command CommandName.UPDATE_LINK" + assert expected in res.json()["description"] + + # Test update link variant returns only modified values + + if study_type == "variant": + res = client.put( + f"/v1/studies/{study_id}/links/{area1_id}/{area2_id}", + json={"hurdlesCost": False}, + ) + assert res.status_code == 200 + + res = client.get(f"/v1/studies/{study_id}/commands") + commands = res.json() + command_args = commands[-1]["args"] + assert command_args["parameters"] == {"hurdles_cost": False} + @pytest.mark.parametrize("study_type", ["raw", "variant"]) def test_link_820(self, client: TestClient, user_access_token: str, study_type: str) -> None: client.headers = {"Authorization": f"Bearer {user_access_token}"} # type: ignore diff --git a/tests/storage/business/test_export.py b/tests/storage/business/test_export.py index e9e37211e1..2fb4b91411 100644 --- a/tests/storage/business/test_export.py +++ b/tests/storage/business/test_export.py @@ -19,6 +19,7 @@ from py7zr import SevenZipFile from antarest.core.config import Config, StorageConfig +from antarest.core.utils.archives import ArchiveFormat, archive_dir from antarest.study.model import DEFAULT_WORKSPACE_NAME, RawStudy from antarest.study.storage.rawstudy.raw_study_service import RawStudyService @@ -162,3 +163,9 @@ def test_export_output(tmp_path: Path): zipf = ZipFile(export_path) assert "file_output.txt" in zipf.namelist() + + # asserts exporting a zipped output doesn't raise an error + output_path = root / "output" / output_id + target_path = root / "output" / f"{output_id}.zip" + archive_dir(output_path, target_path, True, ArchiveFormat.ZIP) + study_service.export_output(study, output_id, export_path) diff --git a/tests/storage/integration/test_STA_mini.py b/tests/storage/integration/test_STA_mini.py index 35aaa4092d..7b1c1ee690 100644 --- a/tests/storage/integration/test_STA_mini.py +++ b/tests/storage/integration/test_STA_mini.py @@ -444,6 +444,10 @@ def test_sta_mini_input(storage_service, url: str, expected_output: dict): f"/v1/studies/{UUID}/raw?path=output/20201014-1422eco-hello/info/general/version", 700, ), + ( + f"/v1/studies/{UUID}/raw?path=output/20201014-1430adq-2/about-the-study/areas", + b"DE\r\nES\r\nFR\r\nIT\r\n", + ), ], ) def test_sta_mini_output(storage_service, url: str, expected_output: dict): diff --git a/tests/storage/test_service.py b/tests/storage/test_service.py index 526bdc679c..8359bd063f 100644 --- a/tests/storage/test_service.py +++ b/tests/storage/test_service.py @@ -75,7 +75,12 @@ from antarest.study.storage.rawstudy.model.filesystem.raw_file_node import RawFileNode 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.utils import assert_permission, assert_permission_on_studies, study_matcher +from antarest.study.storage.utils import ( + assert_permission, + assert_permission_on_studies, + is_output_archived, + study_matcher, +) from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants from antarest.study.storage.variantstudy.model.command_context import CommandContext from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy @@ -1297,19 +1302,14 @@ def test_edit_study_with_command() -> None: study_service.get_raw.return_value = file_study service.storage_service.get_storage = Mock(return_value=study_service) - service._edit_study_using_command(study=Mock(), url="", data=[]) - command.apply.assert_called_with(file_study) + service._edit_study_using_command(study=Mock(spec=RawStudy), url="", data=[]) + command.apply.assert_called_with(file_study, None) study_service = Mock(spec=VariantStudyService) study_service.get_raw.return_value = file_study service.storage_service.get_storage = Mock(return_value=study_service) service._edit_study_using_command(study=Mock(), url="", data=[]) - - study_service.append_command.assert_called_once_with( - study_id=study_id, - command=command.to_dto(), - params=RequestParameters(user=DEFAULT_ADMIN_USER), - ) + service.storage_service.variant_study_service.append_commands.assert_called_once() @pytest.mark.unit_test @@ -2017,3 +2017,19 @@ def test_upgrade_study__raw_study__failed(tmp_path: Path) -> None: # No event must be emitted event_bus.push.assert_not_called() + + +@pytest.mark.unit_test +def test_is_output_archived(tmp_path) -> None: + assert not is_output_archived(path_output=Path("fake_path")) + assert is_output_archived(path_output=Path("fake_path.zip")) + + zipped_output_path = tmp_path / "output.zip" + zipped_output_path.mkdir(parents=True) + assert is_output_archived(path_output=zipped_output_path) + assert is_output_archived(path_output=tmp_path / "output") + + zipped_with_suffix = tmp_path / "output_1.4.3.zip" + zipped_with_suffix.mkdir(parents=True) + assert is_output_archived(path_output=zipped_with_suffix) + assert is_output_archived(path_output=tmp_path / "output_1.4.3") diff --git a/tests/study/storage/variantstudy/test_snapshot_generator.py b/tests/study/storage/variantstudy/test_snapshot_generator.py index 62d173a57c..70a05c391a 100644 --- a/tests/study/storage/variantstudy/test_snapshot_generator.py +++ b/tests/study/storage/variantstudy/test_snapshot_generator.py @@ -996,52 +996,6 @@ def test_generate__nominal_case( # Check: the simulation outputs are not copied. assert not (snapshot_dir / "output").exists() - @with_db_context - def test_generate__with_user_dir( - self, - variant_study: VariantStudy, - variant_study_service: VariantStudyService, - jwt_user: JWTUser, - ) -> None: - """ - Test the generation of a variant study containing a user directory. - We expect that the user directory is correctly preserved. - """ - generator = SnapshotGenerator( - cache=variant_study_service.cache, - raw_study_service=variant_study_service.raw_study_service, - command_factory=variant_study_service.command_factory, - study_factory=variant_study_service.study_factory, - patch_service=variant_study_service.patch_service, - repository=variant_study_service.repository, - ) - - # Generate the snapshot once - generator.generate_snapshot( - variant_study.id, - jwt_user, - denormalize=False, - from_scratch=False, - ) - - # Add a user directory to the variant study. - user_dir = Path(variant_study.snapshot_dir) / "user" - user_dir.mkdir(parents=True, exist_ok=True) - user_dir.joinpath("user_file.txt").touch() - - # Generate the snapshot again - generator.generate_snapshot( - variant_study.id, - jwt_user, - denormalize=False, - from_scratch=False, - ) - - # Check that the user directory is correctly preserved. - user_dir = Path(variant_study.snapshot_dir) / "user" - assert user_dir.is_dir() - assert user_dir.joinpath("user_file.txt").exists() - @with_db_context def test_generate__with_denormalize_true( self, diff --git a/tests/variantstudy/model/command/test_create_link.py b/tests/variantstudy/model/command/test_create_link.py index c2e49e6509..bd4bac60ef 100644 --- a/tests/variantstudy/model/command/test_create_link.py +++ b/tests/variantstudy/model/command/test_create_link.py @@ -17,6 +17,7 @@ import pytest from pydantic import ValidationError +from antarest.core.exceptions import LinkValidationError from antarest.study.business.link_management import LinkInternal from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.ini_reader import IniReader @@ -136,8 +137,8 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): "area1": area1_id, "area2": area2_id, "parameters": {}, - "series": [[0]], "command_context": command_context, + "series": [[0]], "study_version": study_version, } ).apply(study_data=empty_study) @@ -165,8 +166,8 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): "area1": area3_id, "area2": area1_id, "parameters": parameters, - "series": [[0]], "command_context": command_context, + "series": [[0]], "study_version": study_version, } ) @@ -175,6 +176,17 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): ) assert output.status + with pytest.raises(LinkValidationError): + CreateLink.model_validate( + { + "area1": area3_id, + "area2": area1_id, + "parameters": parameters, + "direct": [[0]], + "command_context": command_context, + "study_version": study_version, + } + ) assert (study_path / "input" / "links" / area1_id / f"{area3_id}.txt.link").exists() @@ -201,8 +213,8 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): area1="does_not_exist", area2=area2_id, parameters={}, - series=[[0]], command_context=command_context, + series=[[0]], study_version=study_version, ).apply(empty_study) assert not output.status diff --git a/tests/variantstudy/model/command/test_remove_area.py b/tests/variantstudy/model/command/test_remove_area.py index 014d9f9e5f..b3de1c3342 100644 --- a/tests/variantstudy/model/command/test_remove_area.py +++ b/tests/variantstudy/model/command/test_remove_area.py @@ -135,8 +135,8 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): area2=area_id2, parameters={}, command_context=command_context, - study_version=study_version, series=[[0]], + study_version=study_version, ) output = create_link_command.apply(study_data=empty_study) assert output.status, output.message diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index 10846b389a..e031d07e1e 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -17,7 +17,6 @@ from unittest.mock import Mock import pytest -from antares.study.version import StudyVersion from antarest.matrixstore.service import MatrixService from antarest.study.model import STUDY_VERSION_8_8 @@ -86,6 +85,18 @@ ], study_version=STUDY_VERSION_8_8, ), + CommandDTO( + action=CommandName.UPDATE_LINK.value, + args=[ + { + "area1": "area1", + "area2": "area2", + "parameters": {}, + "series": "series", + } + ], + study_version=STUDY_VERSION_8_8, + ), CommandDTO( action=CommandName.REMOVE_LINK.value, args={ @@ -387,6 +398,21 @@ CommandDTO( action=CommandName.GENERATE_THERMAL_CLUSTER_TIMESERIES.value, args=[{}], study_version=STUDY_VERSION_8_8 ), + CommandDTO( + action=CommandName.CREATE_USER_RESOURCE.value, + args=[{"data": {"path": "folder_1", "resource_type": "folder"}}], + study_version=STUDY_VERSION_8_8, + ), + CommandDTO( + action=CommandName.REMOVE_USER_RESOURCE.value, + args=[{"data": {"path": "folder_1"}}], + study_version=STUDY_VERSION_8_8, + ), + CommandDTO( + action=CommandName.REMOVE_USER_RESOURCE.value, + args=[{"data": {"path": "file_1.txt"}}], + study_version=STUDY_VERSION_8_8, + ), ] @@ -414,7 +440,7 @@ def _get_command_classes(self) -> Set[str]: f".{name}", package="antarest.study.storage.variantstudy.model.command", ) - abstract_commands = {"AbstractBindingConstraintCommand"} + abstract_commands = {"AbstractBindingConstraintCommand", "AbstractLinkCommand"} return {cmd.__name__ for cmd in ICommand.__subclasses__() if cmd.__name__ not in abstract_commands} def test_all_commands_are_tested(self, command_factory: CommandFactory): diff --git a/webapp/package-lock.json b/webapp/package-lock.json index b4f6734d44..972dba194a 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -1,12 +1,12 @@ { "name": "antares-web", - "version": "2.17.6", + "version": "2.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "antares-web", - "version": "2.17.6", + "version": "2.18.0", "dependencies": { "@emotion/react": "11.13.3", "@emotion/styled": "11.13.0", diff --git a/webapp/package.json b/webapp/package.json index 113cdf9238..f4702d8067 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "antares-web", - "version": "2.17.6", + "version": "2.18.0", "private": true, "type": "module", "scripts": { diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index f1b86728c2..952091b0d4 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -526,7 +526,7 @@ "study.modelization.clusters.costGeneration": "TS Cost", "study.modelization.clusters.efficiency": "Efficiency (%)", "study.modelization.clusters.timeSeriesGen": "Time-Series generation", - "study.modelization.clusters.genTs": "Generate Time-Series", + "study.modelization.clusters.genTs": "Parameter", "study.modelization.clusters.volatilityForced": "Volatility forced", "study.modelization.clusters.volatilityPlanned": "Volatility planned", "study.modelization.clusters.lawForced": "Law forced", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index ce4885b1cd..81ffca6679 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -526,11 +526,11 @@ "study.modelization.clusters.costGeneration": "Coût de génération", "study.modelization.clusters.efficiency": "Rendement (%)", "study.modelization.clusters.timeSeriesGen": "Génération des Séries temporelles", - "study.modelization.clusters.genTs": "Générer des Séries temporelles", - "study.modelization.clusters.volatilityForced": "Volatilité forcée", - "study.modelization.clusters.volatilityPlanned": "Volatilité prévue", - "study.modelization.clusters.lawForced": "Loi forcée", - "study.modelization.clusters.lawPlanned": "Loi planifiée", + "study.modelization.clusters.genTs": "Paramètre", + "study.modelization.clusters.volatilityForced": "Arrêts fortuits – Volatilité", + "study.modelization.clusters.volatilityPlanned": "Arrêts planifiés – Volatilité", + "study.modelization.clusters.lawForced": "Loi des arrêts fortuits", + "study.modelization.clusters.lawPlanned": "Loi des arrêts planifiés", "study.modelization.clusters.matrix.common": "Common", "study.modelization.clusters.matrix.tsGen": "TS generator", "study.modelization.clusters.matrix.timeSeries": "Séries temporelles", diff --git a/webapp/src/components/App/Data/MatrixDialog.tsx b/webapp/src/components/App/Data/MatrixDialog.tsx index c02bf8a6c1..4d2b0b7765 100644 --- a/webapp/src/components/App/Data/MatrixDialog.tsx +++ b/webapp/src/components/App/Data/MatrixDialog.tsx @@ -12,67 +12,35 @@ * This file is part of the Antares project. */ -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 } from "../../../common/types"; +import BasicDialog from "@/components/common/dialogs/BasicDialog"; +import MatrixContent from "@/components/common/dialogs/DatabaseUploadDialog/components/MatrixContent"; +import { Button } from "@mui/material"; interface PropTypes { - matrixInfo: MatrixInfoDTO; + matrix: MatrixInfoDTO; open: boolean; onClose: () => void; } -function MatrixDialog(props: PropTypes) { - const { matrixInfo, open, onClose } = props; +function MatrixDialog({ matrix, open, onClose }: PropTypes) { const [t] = useTranslation(); - const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); - const [loading, setLoading] = useState(false); - const [matrix, setMatrix] = useState({ - index: [], - columns: [], - data: [], - }); - useEffect(() => { - const init = async () => { - try { - setLoading(true); - if (matrixInfo) { - const res = await getMatrix(matrixInfo.id); - const matrixContent: MatrixType = { - index: matrix ? res.index : [], - columns: matrix ? res.columns : [], - data: matrix ? res.data : [], - }; - setMatrix(matrixContent); - } - } catch (error) { - enqueueErrorSnackbar(t("data.error.matrix"), error as AxiosError); - } finally { - setLoading(false); - } - }; - init(); - return () => { - setMatrix({ index: [], columns: [], data: [] }); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [enqueueErrorSnackbar, matrixInfo, t]); - - return open ? ( - - ) : ( -
+ actions={} + maxWidth="xl" + fullWidth + contentProps={{ + sx: { p: 1, height: "95vh", width: 1 }, + }} + > + + ); } diff --git a/webapp/src/components/App/Data/index.tsx b/webapp/src/components/App/Data/index.tsx index 9055a3e2b4..6b7fd4469b 100644 --- a/webapp/src/components/App/Data/index.tsx +++ b/webapp/src/components/App/Data/index.tsx @@ -259,14 +259,14 @@ function Data() { {!loaded && } {matrixModal && currentMatrix && ( Otherwise previous data are still present - matrixInfo={currentMatrix} + matrix={currentMatrix} + open={matrixModal} onClose={onMatrixModalClose} /> )} {openModal && ( Otherwise previous data are still present + open={openModal} data={currentData} onNewDataUpdate={onNewDataUpdate} onClose={onModalClose} diff --git a/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/CommandMatrixViewer.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/CommandMatrixViewer.tsx index d57efcec10..51fbb8a7f4 100644 --- a/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/CommandMatrixViewer.tsx +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/CommandMatrixViewer.tsx @@ -43,7 +43,7 @@ function CommandMatrixViewer(props: PropTypes) { {t("data.viewMatrix")} { setOpenViewer(false); diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx index 85909f47e3..fac74da93c 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx @@ -12,13 +12,44 @@ * This file is part of the Antares project. */ -import { Box } from "@mui/material"; +import { Box, IconButton } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { DataType, Timestep } from "./utils"; +import { DataType, matchesSearchTerm, Timestep } from "./utils"; import BooleanFE from "../../../../../common/fieldEditors/BooleanFE"; import SelectFE from "../../../../../common/fieldEditors/SelectFE"; import NumberFE from "../../../../../common/fieldEditors/NumberFE"; import DownloadMatrixButton from "../../../../../common/buttons/DownloadMatrixButton"; +import CheckBoxFE from "@/components/common/fieldEditors/CheckBoxFE"; +import SearchFE from "@/components/common/fieldEditors/SearchFE"; +import { clamp, equals } from "ramda"; +import { useState, useMemo, useEffect, ChangeEvent } from "react"; +import { FilterListOff } from "@mui/icons-material"; +import { useDebouncedField } from "@/hooks/useDebouncedField"; + +interface ColumnHeader { + variable: string; + unit: string; + stat: string; + original: string[]; +} + +interface Filters { + search: string; + exp: boolean; + min: boolean; + max: boolean; + std: boolean; + values: boolean; +} + +const defaultFilters = { + search: "", + exp: true, + min: true, + max: true, + std: true, + values: true, +} as const; interface Props { year: number; @@ -30,6 +61,8 @@ interface Props { maxYear: number; studyId: string; path: string; + colHeaders: string[][]; + onColHeadersChange: (colHeaders: string[][]) => void; } function ResultFilters({ @@ -42,33 +75,192 @@ function ResultFilters({ maxYear, studyId, path, + colHeaders, + onColHeadersChange, }: Props) { const { t } = useTranslation(); + const [filters, setFilters] = useState(defaultFilters); + + const { localValue: localYear, handleChange: debouncedYearChange } = + useDebouncedField({ + value: year, + onChange: setYear, + delay: 500, + transformValue: (value: number) => clamp(1, maxYear, value), + }); + + const filtersApplied = useMemo(() => { + return !equals(filters, defaultFilters); + }, [filters]); + + const parsedHeaders = useMemo(() => { + return colHeaders.map( + (header): ColumnHeader => ({ + variable: String(header[0]).trim(), + unit: String(header[1]).trim(), + stat: String(header[2]).trim(), + original: header, + }), + ); + }, [colHeaders]); + + useEffect(() => { + const filteredHeaders = parsedHeaders.filter((header) => { + // Apply search filters + if (filters.search) { + const matchesVariable = matchesSearchTerm( + header.variable, + filters.search, + ); + + const matchesUnit = matchesSearchTerm(header.unit, filters.search); - const handleYearChange = (event: React.ChangeEvent) => { - const newValue = event.target.value; - - // Allow empty string (when backspacing) - if (newValue === "") { - setYear(1); // Reset to minimum value - return; - } - - const numValue = Number(newValue); - - // Validate the number is within bounds - if (!isNaN(numValue)) { - if (numValue < 1) { - setYear(1); - } else if (numValue > maxYear) { - setYear(maxYear); - } else { - setYear(numValue); + if (!matchesVariable && !matchesUnit) { + return false; + } } - } + + // Apply stat filters + if (header.stat) { + const stat = header.stat.toLowerCase(); + + if (!filters.exp && stat.includes("exp")) { + return false; + } + if (!filters.min && stat.includes("min")) { + return false; + } + if (!filters.max && stat.includes("max")) { + return false; + } + if (!filters.std && stat.includes("std")) { + return false; + } + if (!filters.values && stat.includes("values")) { + return false; + } + } + + return true; + }); + + onColHeadersChange(filteredHeaders.map((h) => h.original)); + }, [filters, parsedHeaders, onColHeadersChange]); + + //////////////////////////////////////////////////////////////// + // Event handlers + //////////////////////////////////////////////////////////////// + + const handleYearChange = (event: ChangeEvent) => { + const value = Number(event.target.value); + debouncedYearChange(value); + }; + + const handleSearchChange = (value: string) => { + setFilters((prev) => ({ ...prev, search: value })); + }; + + const handleStatFilterChange = (stat: keyof Omit) => { + setFilters((prev) => ({ ...prev, [stat]: !prev[stat] })); + }; + + const handleReset = () => { + setFilters(defaultFilters); }; - const FILTERS = [ + //////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////// + + // Local filters (immediately applied on columns headers) + const COLUMN_FILTERS = [ + { + id: "search", + label: "", + field: ( + + handleSearchChange(e.target.value)} + size="small" + /> + + ), + }, + { + id: "exp", + label: "Exp", + field: ( + handleStatFilterChange("exp")} + size="small" + /> + ), + }, + { + id: "min", + label: "Min", + field: ( + handleStatFilterChange("min")} + size="small" + /> + ), + }, + { + id: "max", + label: "Max", + field: ( + handleStatFilterChange("max")} + size="small" + /> + ), + }, + { + id: "std", + label: "Std", + field: ( + handleStatFilterChange("std")} + size="small" + /> + ), + }, + { + id: "values", + label: "Values", + field: ( + handleStatFilterChange("values")} + size="small" + /> + ), + }, + { + id: "reset", + label: "", + field: ( + + + + ), + }, + ] as const; + + // Data filters (requiring API calls, refetch new result) + const RESULT_FILTERS = [ { label: `${t("study.results.mc")}:`, field: ( @@ -79,15 +271,13 @@ function ResultFilters({ falseText="Year by year" size="small" variant="outlined" - onChange={(event) => { - setYear(event?.target.value ? -1 : 1); - }} + onChange={(event) => setYear(event?.target.value ? -1 : 1)} /> - {year > 0 && ( + {localYear > 0 && ( { - setDataType(event?.target.value as DataType); - }} + onChange={(event) => setDataType(event?.target.value as DataType)} /> ), }, @@ -133,13 +321,11 @@ function ResultFilters({ ]} size="small" variant="outlined" - onChange={(event) => { - setTimestep(event?.target.value as Timestep); - }} + onChange={(event) => setTimestep(event?.target.value as Timestep)} /> ), }, - ]; + ] as const; //////////////////////////////////////////////////////////////// // JSX @@ -150,27 +336,54 @@ function ResultFilters({ sx={{ display: "flex", alignItems: "center", - justifyContent: "flex-end", - gap: 2, + justifyContent: "space-between", flexWrap: "wrap", py: 1, }} > - {FILTERS.map(({ label, field }) => ( - - - {label} + {/* Column Filters Group */} + + {COLUMN_FILTERS.map(({ id, label, field }) => ( + + {label} + {field} - {field} - - ))} - + ))} + + + {/* Result Filters Group with Download Button */} + + {RESULT_FILTERS.map(({ label, field }) => ( + + {label} + {field} + + ))} + + ); } diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index 14cc0df972..3aaaf4509b 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -26,7 +26,6 @@ import GridOffIcon from "@mui/icons-material/GridOff"; import { Area, LinkElement, - MatrixType, StudyMetadata, } from "../../../../../../common/types"; import usePromise from "../../../../../../hooks/usePromise"; @@ -57,6 +56,7 @@ import MatrixGrid from "../../../../../common/Matrix/components/MatrixGrid/index import { generateCustomColumns, generateDateTime, + generateResultColumns, groupResultColumns, } from "../../../../../common/Matrix/shared/utils.ts"; import { Column } from "@/components/common/Matrix/shared/constants.ts"; @@ -65,6 +65,8 @@ import ResultFilters from "./ResultFilters.tsx"; import { toError } from "../../../../../../utils/fnUtils.ts"; import EmptyView from "../../../../../common/page/SimpleContent.tsx"; import { getStudyMatrixIndex } from "../../../../../../services/api/matrix.ts"; +import { MatrixGridSynthesis } from "@/components/common/Matrix/components/MatrixGridSynthesis"; +import { ResultMatrixDTO } from "@/components/common/Matrix/shared/types.ts"; function ResultDetails() { const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -82,6 +84,7 @@ function ResultDetails() { const [itemType, setItemType] = useState(OutputItemType.Areas); const [selectedItemId, setSelectedItemId] = useState(""); const [searchValue, setSearchValue] = useState(""); + const [resultColHeaders, setResultColHeaders] = useState([]); const isSynthesis = itemType === OutputItemType.Synthesis; const { t } = useTranslation(); const navigate = useNavigate(); @@ -133,20 +136,23 @@ function ResultDetails() { return ""; }, [output, selectedItem, isSynthesis, dataType, timestep, year]); - const matrixRes = usePromise( + const matrixRes = usePromise( async () => { - if (path) { - const res = await getStudyData(study.id, path); - if (typeof res === "string") { - const fixed = res - .replace(/NaN/g, '"NaN"') - .replace(/Infinity/g, '"Infinity"'); + if (!path) { + return undefined; + } + + const res = await getStudyData(study.id, path); + // TODO add backend parse + if (typeof res === "string") { + const fixed = res + .replace(/NaN/g, '"NaN"') + .replace(/Infinity/g, '"Infinity"'); - return JSON.parse(fixed); - } - return res; + return JSON.parse(fixed); } - return null; + + return res; }, { resetDataOnReload: true, @@ -155,21 +161,13 @@ function ResultDetails() { }, ); - const { data: dateTimeMetadata } = usePromise( - () => getStudyMatrixIndex(study.id, path), - { - deps: [study.id, path], - }, - ); - - const dateTime = dateTimeMetadata && generateDateTime(dateTimeMetadata); - const synthesisRes = usePromise( () => { if (outputId && selectedItem && isSynthesis) { const path = `output/${outputId}/economy/mc-all/grid/${selectedItem.id}`; return getStudyData(study.id, path); } + return Promise.resolve(null); }, { @@ -177,6 +175,31 @@ function ResultDetails() { }, ); + const { data: dateTimeMetadata } = usePromise( + () => getStudyMatrixIndex(study.id, path), + { + deps: [study.id, path], + }, + ); + + const dateTime = dateTimeMetadata && generateDateTime(dateTimeMetadata); + + const resultColumns = useMemo(() => { + if (!matrixRes.data) { + return []; + } + + return groupResultColumns([ + { + id: "date", + title: "Date", + type: Column.DateTime, + editable: false, + }, + ...generateResultColumns(resultColHeaders), + ]); + }, [matrixRes.data, resultColHeaders]); + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// @@ -241,73 +264,77 @@ function ResultDetails() { sx={{ display: "flex", flexDirection: "column", - p: 2, + p: 1, }} > - {isSynthesis ? ( } ifFulfilled={(matrix) => matrix && ( - ) } /> ) : ( - } - ifFulfilled={([, matrix]) => - matrix && ( - + + ( + + )} + ifFulfilled={([, matrix]) => + matrix && ( + <> + {resultColHeaders.length === 0 ? ( + + ) : ( + + )} + + ) + } + ifRejected={(err) => ( + - ) - } - ifRejected={(err) => ( - - )} - /> + )} + /> + )} diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts index 042cc80bba..c72001ef14 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts @@ -82,3 +82,12 @@ export const SYNTHESIS_ITEMS = [ label: "Thermal synthesis", }, ]; + +// Allow the possibilty to use OR operator on search using pipe +export function matchesSearchTerm(text: string, searchTerm: string): boolean { + const searchTerms = searchTerm + .split("|") + .map((term) => term.trim().toLowerCase()); + + return searchTerms.some((term) => text.toLowerCase().includes(term)); +} 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 43cc113532..e9880194d4 100644 --- a/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx @@ -19,7 +19,7 @@ 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 { StudyMetadata } from "../../../../../../common/types"; import { XpansionCandidate } from "../types"; import { getAllCandidates, @@ -44,6 +44,7 @@ import DataViewerDialog from "../../../../../common/dialogs/DataViewerDialog"; import EmptyView from "../../../../../common/page/SimpleContent"; import SplitView from "../../../../../common/SplitView"; import { getLinks } from "@/services/api/studies/links"; +import { MatrixDataDTO } from "@/components/common/Matrix/shared/types"; function Candidates() { const [t] = useTranslation(); @@ -55,7 +56,7 @@ function Candidates() { const [selectedItem, setSelectedItem] = useState(); const [capacityViewDialog, setCapacityViewDialog] = useState<{ filename: string; - content: MatrixType; + content: MatrixDataDTO; }>(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const { enqueueSnackbar } = useSnackbar(); @@ -267,7 +268,6 @@ function Candidates() { )} {!!capacityViewDialog && ( setCapacityViewDialog(undefined)} diff --git a/webapp/src/components/App/Singlestudy/explore/Xpansion/FileList.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/FileList.tsx index e19cc7e598..fcb2463ebf 100644 --- a/webapp/src/components/App/Singlestudy/explore/Xpansion/FileList.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/FileList.tsx @@ -17,13 +17,14 @@ 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 { StudyMetadata } from "../../../../../common/types"; import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; import DataViewerDialog from "../../../../common/dialogs/DataViewerDialog"; import FileTable from "../../../../common/FileTable"; import { Title } from "./share/styles"; import usePromiseWithSnackbarError from "../../../../../hooks/usePromiseWithSnackbarError"; import UsePromiseCond from "../../../../common/utils/UsePromiseCond"; +import { MatrixDataDTO } from "@/components/common/Matrix/shared/types"; interface PropTypes { addResource: (studyId: string, file: File) => Promise; @@ -31,7 +32,7 @@ interface PropTypes { fetchResourceContent: ( studyId: string, filename: string, - ) => Promise; + ) => Promise; listResources: (studyId: string) => Promise; errorMessages?: { add?: string; @@ -57,7 +58,7 @@ function FileList(props: PropTypes) { } = props; const [viewDialog, setViewDialog] = useState<{ filename: string; - content: MatrixType | string; + content: MatrixDataDTO | string; }>(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); @@ -151,7 +152,6 @@ function FileList(props: PropTypes) { /> {!!viewDialog && ( setViewDialog(undefined)} diff --git a/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx index a3b01a4b9b..e418bb0220 100644 --- a/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx @@ -178,7 +178,6 @@ function Settings() { )} {!!resourceViewDialog && ( setResourceViewDialog(undefined)} diff --git a/webapp/src/components/App/Studies/HeaderTopRight.tsx b/webapp/src/components/App/Studies/HeaderTopRight.tsx index 117658f834..24d30de6ea 100644 --- a/webapp/src/components/App/Studies/HeaderTopRight.tsx +++ b/webapp/src/components/App/Studies/HeaderTopRight.tsx @@ -18,13 +18,13 @@ import { useTranslation } from "react-i18next"; import AddCircleOutlineOutlinedIcon from "@mui/icons-material/AddCircleOutlineOutlined"; import UploadOutlinedIcon from "@mui/icons-material/UploadOutlined"; import { createStudy } from "../../../redux/ducks/studies"; -import ImportDialog from "../../common/dialogs/ImportDialog"; +import UploadDialog from "../../common/dialogs/UploadDialog"; import CreateStudyDialog from "./CreateStudyDialog"; import useAppDispatch from "../../../redux/hooks/useAppDispatch"; function HeaderRight() { const [openCreateDialog, setOpenCreateDialog] = useState(false); - const [openImportDialog, setOpenImportDialog] = useState(false); + const [openUploadDialog, setOpenUploadDialog] = useState(false); const [t] = useTranslation(); const dispatch = useAppDispatch(); @@ -54,7 +54,7 @@ function HeaderRight() { variant="outlined" color="primary" startIcon={} - onClick={() => setOpenImportDialog(true)} + onClick={() => setOpenUploadDialog(true)} > {t("global.import")} @@ -73,12 +73,12 @@ function HeaderRight() { onClose={() => setOpenCreateDialog(false)} /> )} - {openImportDialog && ( - setOpenImportDialog(false)} + onCancel={() => setOpenUploadDialog(false)} onImport={handleImport} /> )} diff --git a/webapp/src/components/App/Studies/StudyCard/ActionsMenu.tsx b/webapp/src/components/App/Studies/StudyCard/ActionsMenu.tsx new file mode 100644 index 0000000000..8c1b1cee79 --- /dev/null +++ b/webapp/src/components/App/Studies/StudyCard/ActionsMenu.tsx @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2024, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; +import { archiveStudy, copyStudy, unarchiveStudy } from "@/services/api/study"; +import { + ListItemIcon, + ListItemText, + Menu, + MenuItem, + type MenuProps, +} from "@mui/material"; +import ArchiveOutlinedIcon from "@mui/icons-material/ArchiveOutlined"; +import UnarchiveOutlinedIcon from "@mui/icons-material/UnarchiveOutlined"; +import DownloadOutlinedIcon from "@mui/icons-material/DownloadOutlined"; +import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; +import BoltIcon from "@mui/icons-material/Bolt"; +import FileCopyOutlinedIcon from "@mui/icons-material/FileCopyOutlined"; +import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; +import DriveFileMoveIcon from "@mui/icons-material/DriveFileMove"; +import debug from "debug"; +import { useTranslation } from "react-i18next"; +import type { StudyMetadata } from "@/common/types"; +import type { DialogsType } from "./types"; +import type { SvgIconComponent } from "@mui/icons-material"; + +const logError = debug("antares:studieslist:error"); + +interface Props { + anchorEl: MenuProps["anchorEl"]; + onClose: VoidFunction; + study: StudyMetadata; + setStudyToLaunch: (id: StudyMetadata["id"]) => void; + setOpenDialog: (type: DialogsType) => void; +} + +function ActionsMenu(props: Props) { + const { anchorEl, onClose, study, setStudyToLaunch, setOpenDialog } = props; + const { t } = useTranslation(); + const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); + + //////////////////////////////////////////////////////////////// + // Events Handlers + //////////////////////////////////////////////////////////////// + + const handleLaunchClick = () => { + setStudyToLaunch(study.id); + onClose(); + }; + + const handleUnarchiveClick = () => { + unarchiveStudy(study.id).catch((err) => { + enqueueErrorSnackbar( + t("studies.error.unarchive", { studyname: study.name }), + err, + ); + logError("Failed to unarchive study", study, err); + }); + + onClose(); + }; + + const handleArchiveClick = () => { + archiveStudy(study.id).catch((err) => { + enqueueErrorSnackbar( + t("studies.error.archive", { studyname: study.name }), + err, + ); + logError("Failed to archive study", study, err); + }); + + onClose(); + }; + + const handleCopyClick = () => { + copyStudy( + study.id, + `${study.name} (${t("studies.copySuffix")})`, + false, + ).catch((err) => { + enqueueErrorSnackbar(t("studies.error.copyStudy"), err); + logError("Failed to copy study", study, err); + }); + + onClose(); + }; + + const handleMoveClick = () => { + setOpenDialog("move"); + onClose(); + }; + + const handlePropertiesClick = () => { + setOpenDialog("properties"); + onClose(); + }; + + const handleExportClick = () => { + setOpenDialog("export"); + onClose(); + }; + + const handleDeleteClick = () => { + setOpenDialog("delete"); + onClose(); + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + const menuItem = ( + show: boolean, + text: string, + Icon: SvgIconComponent, + onClick: VoidFunction, + color?: string, + ) => + show && ( + + + + + {text} + + ); + + return ( + + {[ + menuItem( + !study.archived, + t("global.launch"), + BoltIcon, + handleLaunchClick, + ), + menuItem( + !study.archived, + t("study.properties"), + EditOutlinedIcon, + handlePropertiesClick, + ), + menuItem( + !study.archived, + t("global.copy"), + FileCopyOutlinedIcon, + handleCopyClick, + ), + menuItem( + study.managed, + t("studies.moveStudy"), + DriveFileMoveIcon, + handleMoveClick, + ), + menuItem( + !study.archived, + t("global.export"), + DownloadOutlinedIcon, + handleExportClick, + ), + menuItem( + study.archived, + t("global.unarchive"), + UnarchiveOutlinedIcon, + handleUnarchiveClick, + ), + menuItem( + study.managed && !study.archived, + t("global.archive"), + ArchiveOutlinedIcon, + handleArchiveClick, + ), + menuItem( + study.managed, + t("global.delete"), + DeleteOutlinedIcon, + handleDeleteClick, + "error.light", + ), + ]} + + ); +} + +export default ActionsMenu; diff --git a/webapp/src/components/App/Studies/StudyCard.tsx b/webapp/src/components/App/Studies/StudyCard/index.tsx similarity index 57% rename from webapp/src/components/App/Studies/StudyCard.tsx rename to webapp/src/components/App/Studies/StudyCard/index.tsx index 79c0c6b991..cb74b58819 100644 --- a/webapp/src/components/App/Studies/StudyCard.tsx +++ b/webapp/src/components/App/Studies/StudyCard/index.tsx @@ -24,10 +24,6 @@ import { CardContent, Button, Typography, - Menu, - MenuItem, - ListItemIcon, - ListItemText, Tooltip, Chip, Divider, @@ -37,37 +33,31 @@ import { indigo } from "@mui/material/colors"; import ScheduleOutlinedIcon from "@mui/icons-material/ScheduleOutlined"; import UpdateOutlinedIcon from "@mui/icons-material/UpdateOutlined"; import PersonOutlineIcon from "@mui/icons-material/PersonOutline"; -import DriveFileMoveIcon from "@mui/icons-material/DriveFileMove"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; -import UnarchiveOutlinedIcon from "@mui/icons-material/UnarchiveOutlined"; -import DownloadOutlinedIcon from "@mui/icons-material/DownloadOutlined"; import ArchiveOutlinedIcon from "@mui/icons-material/ArchiveOutlined"; -import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -import BoltIcon from "@mui/icons-material/Bolt"; -import FileCopyOutlinedIcon from "@mui/icons-material/FileCopyOutlined"; import AltRouteOutlinedIcon from "@mui/icons-material/AltRouteOutlined"; import debug from "debug"; import { areEqual } from "react-window"; -import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; -import { StudyMetadata, StudyType } from "../../../common/types"; +import { StudyMetadata, StudyType } from "../../../../common/types"; import { buildModificationDate, convertUTCToLocalTime, displayVersionName, -} from "../../../services/utils"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import ExportModal from "./ExportModal"; -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 PropertiesDialog from "../Singlestudy/PropertiesDialog"; +} from "../../../../services/utils"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import ExportModal from "../ExportModal"; +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 PropertiesDialog from "../../Singlestudy/PropertiesDialog"; +import ActionsMenu from "./ActionsMenu"; +import type { DialogsType } from "./types"; const logError = debug("antares:studieslist:error"); @@ -100,33 +90,30 @@ const StudyCard = memo((props: Props) => { const { enqueueSnackbar } = useSnackbar(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const [anchorEl, setAnchorEl] = useState(null); - const [openMenu, setOpenMenu] = useState(""); - const [openPropertiesDialog, setOpenPropertiesDialog] = useState(false); - const [openConfirmDeleteDialog, setOpenConfirmDeleteDialog] = useState(false); - const [openExportModal, setOpenExportModal] = useState(false); - const [openMoveDialog, setOpenMoveDialog] = useState(false); + const [openDialog, setOpenDialog] = useState(null); const study = useAppSelector((state) => getStudy(state, id)); const isFavorite = useAppSelector((state) => isStudyFavorite(state, id)); const dispatch = useAppDispatch(); const navigate = useNavigate(); + //////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////// + + const closeDialog = () => { + setOpenDialog(null); + }; + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// const handleMenuOpen = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); - setOpenMenu(event.currentTarget.id); }; const handleMenuClose = () => { setAnchorEl(null); - setOpenMenu(""); - }; - - const handleLaunchClick = () => { - setStudyToLaunch(id); - handleMenuClose(); }; const handleFavoriteToggle = () => { @@ -140,28 +127,8 @@ const StudyCard = memo((props: Props) => { enqueueErrorSnackbar(t("studies.error.deleteStudy"), err as AxiosError); logError("Failed to delete study", study, err); }); - setOpenConfirmDeleteDialog(false); - }; - const handleUnarchiveClick = () => { - studyApi.unarchiveStudy(id).catch((err) => { - enqueueErrorSnackbar( - t("studies.error.unarchive", { studyname: study?.name }), - err, - ); - logError("Failed to unarchive study", study, err); - }); - }; - - const handleArchiveClick = () => { - studyApi.archiveStudy(id).catch((err) => { - enqueueErrorSnackbar( - t("studies.error.archive", { studyname: study?.name }), - err, - ); - logError("Failed to archive study", study, err); - }); - handleMenuClose(); + setOpenDialog("delete"); }; const handleCopyId = () => { @@ -178,16 +145,6 @@ const StudyCard = memo((props: Props) => { }); }; - const handleCopyClick = () => { - studyApi - .copyStudy(id, `${study?.name} (${t("studies.copySuffix")})`, false) - .catch((err) => { - enqueueErrorSnackbar(t("studies.error.copyStudy"), err); - logError("Failed to copy study", study, err); - }); - handleMenuClose(); - }; - //////////////////////////////////////////////////////////////// // JSX //////////////////////////////////////////////////////////////// @@ -432,8 +389,6 @@ const StudyCard = memo((props: Props) => { - - {study.archived ? ( - - - - - {t("global.unarchive")} - - ) : ( -
- - - - - {t("global.launch")} - - { - setOpenPropertiesDialog(true); - handleMenuClose(); - }} - > - - - - {t("study.properties")} - - - - - - {t("global.copy")} - - {study.managed && ( - { - setOpenMoveDialog(true); - handleMenuClose(); - }} - > - - - - {t("studies.moveStudy")} - - )} - { - setOpenExportModal(true); - handleMenuClose(); - }} - > - - - - {t("global.export")} - - {study.managed && ( - - - - - {t("global.archive")} - - )} -
- )} - {study.managed && ( - { - setOpenConfirmDeleteDialog(true); - handleMenuClose(); - }} - > - - - - - {t("global.delete")} - - - )} -
- - {openPropertiesDialog && study && ( - setOpenPropertiesDialog(false)} - study={study} - /> - )} - {openConfirmDeleteDialog && ( - setOpenConfirmDeleteDialog(false)} - onConfirm={handleDelete} - alert="warning" - open - > - {t("studies.question.delete")} - - )} - {openExportModal && ( - setOpenExportModal(false)} - study={study} - /> - )} - {openMoveDialog && ( - setOpenMoveDialog(false)} study={study} + setStudyToLaunch={setStudyToLaunch} + setOpenDialog={setOpenDialog} /> - )} + + + + {t("studies.question.delete")} + + + ); }, areEqual); diff --git a/webapp/src/components/App/Studies/StudyCard/types.tsx b/webapp/src/components/App/Studies/StudyCard/types.tsx new file mode 100644 index 0000000000..8c7ff608f5 --- /dev/null +++ b/webapp/src/components/App/Studies/StudyCard/types.tsx @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2024, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +export type DialogsType = "move" | "properties" | "export" | "delete"; diff --git a/webapp/src/components/common/EditableMatrix/MatrixGraphView.tsx b/webapp/src/components/common/EditableMatrix/MatrixGraphView.tsx deleted file mode 100644 index 765f991b03..0000000000 --- a/webapp/src/components/common/EditableMatrix/MatrixGraphView.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright (c) 2024, RTE (https://www.rte-france.com) - * - * See AUTHORS.txt - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - * - * This file is part of the Antares project. - */ - -import { useState } from "react"; -import Plot from "react-plotly.js"; -import AutoSizer from "react-virtualized-auto-sizer"; -import { - Box, - Checkbox, - Chip, - FormControl, - FormControlLabel, - InputLabel, - MenuItem, - OutlinedInput, - Select, - SelectChangeEvent, -} from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { MatrixType } from "../../../common/types"; -import "handsontable/dist/handsontable.min.css"; -import { formatDateFromIndex } from "./utils"; - -interface PropTypes { - matrix: MatrixType; -} - -export default function MatrixGraphView(props: PropTypes) { - const [t] = useTranslation(); - const { matrix } = props; - const { data = [], columns = [], index = [] } = matrix; - const [selectedColumns, setSelectedColumns] = useState([]); - const [monotonic, setMonotonic] = useState(false); - - const handleChange = (event: SelectChangeEvent) => { - setSelectedColumns(event.target.value as number[]); - }; - - const monotonicChange = () => { - setMonotonic(!monotonic); - }; - - const unitChange = (tabBase: number[]) => { - const stepLength = 100 / tabBase.length; - return tabBase.map((el, i) => stepLength * (i + 1)); - }; - - return ( - - - - {t("matrix.graphSelector")} - - - - } - label={t("matrix.monotonicView")} - /> - - - - {({ height, width }) => ( - ({ - x: monotonic - ? unitChange(index as number[]) - : formatDateFromIndex(index), - y: monotonic - ? data.map((a) => a[val]).sort((b, c) => c - b) - : data.map((a) => a[val]), - mode: "lines", - name: `${columns[val]}`, - }))} - layout={{ width, height }} - /> - )} - - - - ); -} diff --git a/webapp/src/components/common/EditableMatrix/index.tsx b/webapp/src/components/common/EditableMatrix/index.tsx deleted file mode 100644 index 1054812b98..0000000000 --- a/webapp/src/components/common/EditableMatrix/index.tsx +++ /dev/null @@ -1,207 +0,0 @@ -/** - * Copyright (c) 2024, RTE (https://www.rte-france.com) - * - * See AUTHORS.txt - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - * - * This file is part of the Antares project. - */ - -import { useEffect, useState, useRef } from "react"; -import debug from "debug"; -import HT from "handsontable"; -import { - MatrixIndex, - MatrixEditDTO, - MatrixType, - MatrixStats, -} from "../../../common/types"; -import "handsontable/dist/handsontable.min.css"; -import { Root } from "./style"; -import { - computeStats, - createDateFromIndex, - cellChangesToMatrixEdits, -} from "./utils"; -import Handsontable, { HotTableClass } from "../Handsontable"; - -const logError = debug("antares:editablematrix:error"); - -interface PropTypes { - matrix: MatrixType; - matrixIndex?: MatrixIndex; - matrixTime: boolean; - readOnly: boolean; - onUpdate?: (change: MatrixEditDTO[], source: string) => void; - columnsNames?: string[] | readonly string[]; - rowNames?: string[]; - computStats?: MatrixStats; - showPercent?: boolean; -} - -type CellType = Array; - -const formatColumnName = (col: string) => { - try { - const colIndex = parseInt(col, 10); - if (!Number.isNaN(colIndex)) { - return `TS-${colIndex + 1}`; - } - } catch (e) { - logError(`Unable to parse matrix column index ${col}`, e); - } - return col; -}; - -function EditableMatrix(props: PropTypes) { - const { - readOnly, - matrix, - matrixIndex, - matrixTime, - onUpdate, - columnsNames, - rowNames, - computStats, - showPercent = false, - } = props; - const { data = [], columns = [], index = [] } = matrix; - const prependIndex = index.length > 0 && matrixTime; - const [grid, setGrid] = useState([]); - const [formattedColumns, setFormattedColumns] = useState( - [], - ); - const hotTableComponent = useRef(null); - - //////////////////////////////////////////////////////////////// - // Event Handlers - //////////////////////////////////////////////////////////////// - - const handleSlice = (changes: HT.CellChange[], source: string) => { - if (!onUpdate) { - return; - } - - const filteredChanges = changes.filter( - ([, , oldValue, newValue]) => - parseFloat(oldValue) !== parseFloat(newValue), - ); - - if (filteredChanges.length > 0) { - const edits = cellChangesToMatrixEdits( - filteredChanges, - matrixTime, - showPercent, - ); - - onUpdate(edits, source); - } - }; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "a" && e.ctrlKey) { - e.preventDefault(); - e.stopImmediatePropagation(); - if (hotTableComponent.current?.hotInstance) { - const hot = hotTableComponent.current.hotInstance; - const cols = computStats === MatrixStats.TOTAL ? 1 : 3; - hot.selectCell( - 0, - prependIndex ? 1 : 0, - hot.countRows() - 1, - hot.countCols() - (computStats ? cols : 0) - 1, - ); - } - } - }; - - useEffect(() => { - setFormattedColumns([ - ...(prependIndex ? [{ title: "Time", readOnly: true, width: 130 }] : []), - ...columns.map((col, index) => ({ - title: columnsNames?.[index] || formatColumnName(col), - readOnly, - })), - ...(computStats === MatrixStats.TOTAL - ? [{ title: "Total", readOnly: true }] - : []), - ...(computStats === MatrixStats.STATS - ? [ - { title: "Min.", readOnly: true }, - { title: "Max.", readOnly: true }, - { title: "Average", readOnly: true }, - ] - : []), - ]); - - const tmpData = data.map((row, i) => { - let tmpRow = row as Array; - if (prependIndex && matrixIndex) { - tmpRow = [createDateFromIndex(i, matrixIndex)].concat(row); - } - - if (computStats) { - tmpRow = tmpRow.concat( - computeStats(computStats, row) as Array, - ); - } - - if (showPercent) { - tmpRow = tmpRow.map((cell) => { - if (typeof cell === "number") { - return cell * 100; - } - return cell; - }); - } - - return tmpRow; - }); - - setGrid(tmpData); - }, [ - columns, - columnsNames, - data, - index, - prependIndex, - readOnly, - matrixIndex, - computStats, - showPercent, - ]); - - const matrixRowNames = - rowNames || (matrixIndex && index.map((i) => String(i))); - - //////////////////////////////////////////////////////////////// - // JSX - //////////////////////////////////////////////////////////////// - - return ( - - - onUpdate && handleSlice(change || [], source) - } - beforeKeyDown={(e) => handleKeyDown(e)} - columns={formattedColumns} - rowHeaders={matrixRowNames || true} - /> - - ); -} - -export default EditableMatrix; diff --git a/webapp/src/components/common/EditableMatrix/style.ts b/webapp/src/components/common/EditableMatrix/style.ts deleted file mode 100644 index dcd6dc3159..0000000000 --- a/webapp/src/components/common/EditableMatrix/style.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2024, RTE (https://www.rte-france.com) - * - * See AUTHORS.txt - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - * - * This file is part of the Antares project. - */ - -import { styled, Box, Button } from "@mui/material"; - -export const Root = styled(Box)(({ theme }) => ({ - width: "100%", - height: "100%", - display: "flex", - flexDirection: "column", - alignItems: "center", - overflow: "hidden", -})); - -export const StyledButton = styled(Button)(({ theme }) => ({ - backgroundColor: "rgba(180, 180, 180, 0.09)", - color: "white", - borderRight: "none !important", - "&:hover": { - color: "white", - backgroundColor: theme.palette.secondary.main, - }, - "&:disabled": { - backgroundColor: theme.palette.secondary.dark, - color: "white !important", - }, -})); diff --git a/webapp/src/components/common/EditableMatrix/utils.ts b/webapp/src/components/common/EditableMatrix/utils.ts deleted file mode 100644 index aed63af7e7..0000000000 --- a/webapp/src/components/common/EditableMatrix/utils.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright (c) 2024, RTE (https://www.rte-france.com) - * - * See AUTHORS.txt - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - * - * This file is part of the Antares project. - */ - -import _ from "lodash"; -import moment, { DurationInputArg2 } from "moment"; -import HT from "handsontable"; -import { - MatrixEditDTO, - MatrixIndex, - MatrixStats, - Operator, - StudyOutputDownloadLevelDTO, -} from "../../../common/types"; - -export const formatDateFromIndex = ( - index: Array, -): string[] => { - if (index.length === 0) { - return []; - } - const sample = index[0]; - const datetimeMatch = String(sample).match(/\d{2}\/\d{2} \d{2}:\d{2}/); - if (!datetimeMatch) { - // daily - const dateMatch = String(sample).match(/(\d{2})\/(\d{2})/); - if (dateMatch) { - return index.map((e) => moment(e, "MM/DD").format("MM/DD HH:mm")); - } - // daily without information - // TODO this should depends on the study general settings (calendar) - // this case is when no such information is available - if (index.length > 100) { - const startDate = moment("01/01/2000 00:00:00", "DD/MM/YYYY hh:mm:ss"); - return index.map((e, i) => - moment.utc(startDate).add(i, "h").format("YYYY/MM/DD HH:mm"), - ); - } - // weekly - if (index.length > 12) { - const startDate = moment(2005, "YYYY").week(sample as number); - return index.map((e, i) => - moment.utc(startDate).add(i, "w").format("YYYY/MM/DD HH:mm"), - ); - } - // monthly - if (index.length > 1) { - return index.map((e) => - moment(_.padStart(String(e), 2, "0"), "MM").format("MM/DD HH:mm"), - ); - } - } - return index.map((e) => String(e)); -}; - -const convertLevelDate = ( - levelDate: StudyOutputDownloadLevelDTO, -): DurationInputArg2 => { - if (levelDate === StudyOutputDownloadLevelDTO.ANNUAL) { - return "year"; - } - if (levelDate === StudyOutputDownloadLevelDTO.DAILY) { - return "day"; - } - if (levelDate === StudyOutputDownloadLevelDTO.HOURLY) { - return "hour"; - } - if (levelDate === StudyOutputDownloadLevelDTO.MONTHLY) { - return "month"; - } - return "week"; -}; - -export const createDateFromIndex = ( - indexDate: string | number, - matrixIndex: MatrixIndex, -): string | number => { - const date = moment - .utc(matrixIndex.start_date) - .add(indexDate, convertLevelDate(matrixIndex.level)) - .format( - matrixIndex.level === StudyOutputDownloadLevelDTO.HOURLY - ? "ddd DD MMM HH:mm" - : "ddd DD MMM", - ); - return date; -}; - -export const cellChangesToMatrixEdits = ( - cellChanges: HT.CellChange[], - matrixTime: boolean, - isPercentEnabled: boolean, -): MatrixEditDTO[] => - cellChanges.map(([row, column, , value]) => { - const rowIndex = parseFloat(row.toString()); - const colIndex = parseFloat(column.toString()) - (matrixTime ? 1 : 0); - - return { - coordinates: [[rowIndex, colIndex]], - operation: { - operation: Operator.EQ, - value: isPercentEnabled ? parseFloat(value) / 100 : parseFloat(value), - }, - }; - }); - -export const computeStats = (statsType: string, row: number[]): number[] => { - if (statsType === MatrixStats.TOTAL) { - return [ - row.reduce((agg, value) => { - return agg + value; - }, 0), - ]; - } - if (statsType === MatrixStats.STATS) { - const statsInfo = row.reduce( - (agg, value) => { - const newAgg = { ...agg }; - if (value < agg.min) { - newAgg.min = value; - } - if (value > agg.max) { - newAgg.max = value; - } - newAgg.total = agg.total + value; - - return newAgg; - }, - { min: row[0], max: row[0], total: 0 }, - ); - return [statsInfo.min, statsInfo.max, statsInfo.total / row.length]; - } - return []; -}; - -export default {}; diff --git a/webapp/src/components/common/FileTable.tsx b/webapp/src/components/common/FileTable.tsx index 9ffac99fd6..4653133322 100644 --- a/webapp/src/components/common/FileTable.tsx +++ b/webapp/src/components/common/FileTable.tsx @@ -37,13 +37,13 @@ import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import ConfirmationDialog from "./dialogs/ConfirmationDialog"; import { GenericInfo } from "../../common/types"; import DownloadLink from "./DownloadLink"; -import ImportDialog from "./dialogs/ImportDialog"; +import UploadDialog from "./dialogs/UploadDialog"; interface PropType { title: ReactNode; content: GenericInfo[]; onDelete?: (id: string) => Promise; - onRead: (id: string) => Promise; + onRead: (id: string) => Promise | void; uploadFile?: (file: File) => Promise; onFileDownload?: (id: string) => string; onAssign?: (id: string) => Promise; @@ -67,7 +67,7 @@ function FileTable(props: PropType) { } = props; const [t] = useTranslation(); const [openConfirmationModal, setOpenConfirmationModal] = useState(""); - const [openImportDialog, setOpenImportDialog] = useState(false); + const [openUploadDialog, setOpenUploadDialog] = useState(false); return ( } - onClick={() => setOpenImportDialog(true)} + onClick={() => setOpenUploadDialog(true)} > {t("global.import")} @@ -226,10 +226,10 @@ function FileTable(props: PropType) { {t("xpansion.question.deleteFile")} )} - {openImportDialog && ( - setOpenImportDialog(false)} + {openUploadDialog && ( + setOpenUploadDialog(false)} onImport={async (file) => uploadFile?.(file)} /> )} diff --git a/webapp/src/components/common/Matrix/components/MatrixActions/index.tsx b/webapp/src/components/common/Matrix/components/MatrixActions/index.tsx index f64c255224..6681d378f4 100644 --- a/webapp/src/components/common/Matrix/components/MatrixActions/index.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixActions/index.tsx @@ -13,14 +13,16 @@ */ import { Box, Divider, IconButton, Tooltip } from "@mui/material"; -import SplitButton from "@/components/common/buttons/SplitButton"; +import SplitButton, { + type SplitButtonProps, +} from "@/components/common/buttons/SplitButton"; import DownloadMatrixButton from "@/components/common/buttons/DownloadMatrixButton"; import { FileDownload, Save, Undo, Redo } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { LoadingButton } from "@mui/lab"; interface MatrixActionsProps { - onImport: VoidFunction; + onImport: SplitButtonProps["onClick"]; onSave: VoidFunction; studyId: string; path: string; diff --git a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx index 07ffd84d2c..688b647cc1 100644 --- a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx @@ -31,7 +31,7 @@ import { } from "../../shared/types"; import { useColumnMapping } from "../../hooks/useColumnMapping"; import { useMatrixPortal } from "../../hooks/useMatrixPortal"; -import { darkTheme, readOnlyDarkTheme } from "./styles"; +import { darkTheme, readOnlyDarkTheme } from "../../styles"; export interface MatrixGridProps { data: number[][]; diff --git a/webapp/src/components/common/Matrix/components/MatrixGrid/styles.ts b/webapp/src/components/common/Matrix/components/MatrixGrid/styles.ts deleted file mode 100644 index 834af21686..0000000000 --- a/webapp/src/components/common/Matrix/components/MatrixGrid/styles.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2024, RTE (https://www.rte-france.com) - * - * See AUTHORS.txt - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - * - * This file is part of the Antares project. - */ - -import { Theme } from "@glideapps/glide-data-grid"; - -export const darkTheme: Theme = { - accentColor: "rgba(255, 184, 0, 0.9)", - accentLight: "rgba(255, 184, 0, 0.2)", - accentFg: "#FFFFFF", - textDark: "#FFFFFF", - textMedium: "#C1C3D9", - textLight: "#A1A5B9", - textBubble: "#FFFFFF", - bgIconHeader: "#1E1F2E", - fgIconHeader: "#FFFFFF", - textHeader: "#FFFFFF", - textGroupHeader: "#C1C3D9", - bgCell: "#262737", // main background color - bgCellMedium: "#2E2F42", - bgHeader: "#1E1F2E", - bgHeaderHasFocus: "#2E2F42", - bgHeaderHovered: "#333447", - bgBubble: "#333447", - bgBubbleSelected: "#3C3E57", - bgSearchResult: "#6366F133", - borderColor: "rgba(255, 255, 255, 0.12)", - drilldownBorder: "rgba(255, 255, 255, 0.35)", - linkColor: "#818CF8", - headerFontStyle: "bold 11px", - baseFontStyle: "13px", - fontFamily: "Inter, sans-serif", - editorFontSize: "13px", - lineHeight: 1.5, - textHeaderSelected: "#FFFFFF", - cellHorizontalPadding: 8, - cellVerticalPadding: 5, - headerIconSize: 16, - markerFontStyle: "normal", -}; - -export const readOnlyDarkTheme: Partial = { - bgCell: "#1A1C2A", - bgCellMedium: "#22243A", - textDark: "#FAF9F6", - textMedium: "#808080", - textLight: "#606060", - accentColor: "#4A4C66", - accentLight: "rgba(74, 76, 102, 0.2)", - borderColor: "rgba(255, 255, 255, 0.08)", - drilldownBorder: "rgba(255, 255, 255, 0.2)", - headerFontStyle: "bold 11px", -}; - -export const aggregatesTheme: Partial = { - bgCell: "#3D3E5F", - bgCellMedium: "#383A5C", - textDark: "#FFFFFF", - fontFamily: "Inter, sans-serif", - baseFontStyle: "13px", - editorFontSize: "13px", - headerFontStyle: "bold 11px", -}; diff --git a/webapp/src/components/common/Matrix/components/MatrixGridSynthesis/index.tsx b/webapp/src/components/common/Matrix/components/MatrixGridSynthesis/index.tsx new file mode 100644 index 0000000000..ad56f83b49 --- /dev/null +++ b/webapp/src/components/common/Matrix/components/MatrixGridSynthesis/index.tsx @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2024, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import DataEditor, { + GridCellKind, + GridColumn, + Item, + NumberCell, + TextCell, +} from "@glideapps/glide-data-grid"; +import { useMemo } from "react"; +import { darkTheme, readOnlyDarkTheme } from "../../styles"; +import { formatGridNumber } from "../../shared/utils"; + +type CellValue = number | string; + +interface MatrixGridSynthesisProps { + data: CellValue[][]; + columns: GridColumn[]; + width?: string | number; + height?: string | number; +} + +export function MatrixGridSynthesis({ + data, + columns, + width = "100%", + height = "100%", +}: MatrixGridSynthesisProps) { + const theme = useMemo( + () => ({ + ...darkTheme, + ...readOnlyDarkTheme, + }), + [], + ); + + const getCellContent = useMemo( + () => (cell: Item) => { + const [col, row] = cell; + const value = data[row]?.[col]; + + if (typeof value === "number") { + return { + kind: GridCellKind.Number, + data: value, + displayData: formatGridNumber({ value, maxDecimals: 3 }), + decimalSeparator: ".", + thousandSeparator: " ", + readonly: true, + allowOverlay: false, + contentAlign: "right", + } satisfies NumberCell; + } + + return { + kind: GridCellKind.Text, + data: String(value ?? ""), + displayData: String(value ?? ""), + readonly: true, + allowOverlay: false, + } satisfies TextCell; + }, + [data], + ); + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ( +
+ +
+ ); +} diff --git a/webapp/src/components/common/Matrix/components/MatrixUpload.tsx b/webapp/src/components/common/Matrix/components/MatrixUpload.tsx new file mode 100644 index 0000000000..4276326208 --- /dev/null +++ b/webapp/src/components/common/Matrix/components/MatrixUpload.tsx @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2024, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import { StudyMetadata } from "@/common/types"; +import DatabaseUploadDialog from "@/components/common/dialogs/DatabaseUploadDialog"; +import { useTranslation } from "react-i18next"; +import UploadDialog from "../../dialogs/UploadDialog"; + +interface MatrixFileOptions { + accept?: Record; + dropzoneText?: string; +} + +interface BaseMatrixUploadProps { + studyId: StudyMetadata["id"]; + path: string; + open: boolean; + onClose: VoidFunction; +} + +interface FileMatrixUploadProps extends BaseMatrixUploadProps { + type: "file"; + onFileUpload: (file: File) => Promise; + fileOptions?: MatrixFileOptions; +} + +interface DatabaseMatrixUploadProps extends BaseMatrixUploadProps { + type: "database"; + onFileUpload?: never; + fileOptions?: never; +} + +type MatrixUploadProps = FileMatrixUploadProps | DatabaseMatrixUploadProps; + +function MatrixUpload({ + studyId, + path, + type, + open, + onClose, + onFileUpload, + fileOptions, +}: MatrixUploadProps) { + const { t } = useTranslation(); + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + // Import from the filesystem + if (type === "file" && onFileUpload) { + return ( + + ); + } + + // Import from the matrix store (database) + if (type === "database") { + return ( + + ); + } + + return null; +} + +export default MatrixUpload; diff --git a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts index 27e727b602..286b732405 100644 --- a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts @@ -43,7 +43,7 @@ import { importFile } from "../../../../../services/api/studies/raw"; import { fetchMatrixFn } from "../../../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; import usePrompt from "../../../../../hooks/usePrompt"; import { Aggregate, Column, Operation } from "../../shared/constants"; -import { aggregatesTheme } from "../../components/MatrixGrid/styles"; +import { aggregatesTheme } from "../../styles"; interface DataState { data: MatrixDataDTO["data"]; @@ -162,12 +162,12 @@ export function useMatrix( }); } - const dataColumns = generateDataColumns( + const dataColumns = generateDataColumns({ timeSeriesColumns, - columnCount, + count: columnCount, customColumns, - colWidth, - ); + width: colWidth, + }); const aggregatesColumns: EnhancedGridColumn[] = aggregateTypes.map( (aggregateType) => ({ @@ -249,7 +249,7 @@ export function useMatrix( applyUpdates(updates); }; - const handleImport = async (file: File) => { + const handleUpload = async (file: File) => { try { await importFile({ file, studyId, path: url }); await fetchMatrix(); @@ -312,12 +312,13 @@ export function useMatrix( dateTime, handleCellEdit, handleMultipleCellsEdit, - handleImport, + handleUpload, handleSaveUpdates, pendingUpdatesCount: currentState.updateCount, undo: handleUndo, redo: handleRedo, canUndo: canUndoChanges, canRedo, + reload: fetchMatrix, }; } diff --git a/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx b/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx index 7dabcb97a5..fc1087a927 100644 --- a/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx @@ -171,7 +171,7 @@ describe("useMatrix", () => { const hook = await setupHook(); await act(async () => { - await hook.result.current.handleImport(mockFile); + await hook.result.current.handleUpload(mockFile); }); expect(rawStudy.importFile).toHaveBeenCalledWith({ diff --git a/webapp/src/components/common/Matrix/index.tsx b/webapp/src/components/common/Matrix/index.tsx index 95a09d6197..1316d2d7f2 100644 --- a/webapp/src/components/common/Matrix/index.tsx +++ b/webapp/src/components/common/Matrix/index.tsx @@ -17,7 +17,6 @@ import MatrixGrid from "./components/MatrixGrid"; import { useMatrix } from "./hooks/useMatrix"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import ImportDialog from "../dialogs/ImportDialog"; import { useOutletContext } from "react-router"; import { StudyMetadata } from "../../../common/types"; import { MatrixContainer, MatrixHeader, MatrixTitle } from "./styles"; @@ -26,6 +25,7 @@ import EmptyView from "../page/SimpleContent"; import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; import { AggregateConfig } from "./shared/types"; import { GridOff } from "@mui/icons-material"; +import MatrixUpload from "@/components/common/Matrix/components/MatrixUpload"; interface MatrixProps { url: string; @@ -60,8 +60,11 @@ function Matrix({ }: MatrixProps) { const { t } = useTranslation(); const { study } = useOutletContext<{ study: StudyMetadata }>(); - const [openImportDialog, setOpenImportDialog] = useState(false); + const [uploadType, setUploadType] = useState<"file" | "database" | undefined>( + undefined, + ); + // TODO: split `useMatrix` into smaller units const { data, aggregates, @@ -72,13 +75,14 @@ function Matrix({ dateTime, handleCellEdit, handleMultipleCellsEdit, - handleImport, + handleUpload, handleSaveUpdates, pendingUpdatesCount, undo, redo, canUndo, canRedo, + reload, } = useMatrix( study.id, url, @@ -108,7 +112,9 @@ function Matrix({ {t(title)} setOpenImportDialog(true)} + onImport={(_, index) => { + setUploadType(index === 0 ? "file" : "database"); + }} onSave={handleSaveUpdates} studyId={study.id} path={url} @@ -139,14 +145,30 @@ function Matrix({ showPercent={showPercent} /> )} - {openImportDialog && ( - setOpenImportDialog(false)} - onImport={handleImport} - accept={{ "text/*": [".csv", ".tsv", ".txt"] }} + {uploadType === "file" && ( + setUploadType(undefined)} + onFileUpload={handleUpload} + fileOptions={{ + accept: { "text/*": [".csv", ".tsv", ".txt"] }, + dropzoneText: t("matrix.message.importHint"), + }} + /> + )} + {uploadType === "database" && ( + { + setUploadType(undefined); + reload(); + }} /> )} diff --git a/webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts b/webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts index 7aee54c414..c7b3afc412 100644 --- a/webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts +++ b/webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts @@ -118,6 +118,7 @@ describe("Matrix Utils", () => { expect(column).toEqual({ id: `custom${index + 1}`, title: titles[index], + width: 100, type: Column.Number, editable: true, }); diff --git a/webapp/src/components/common/Matrix/shared/types.ts b/webapp/src/components/common/Matrix/shared/types.ts index 915602d0e1..6faae0402f 100644 --- a/webapp/src/components/common/Matrix/shared/types.ts +++ b/webapp/src/components/common/Matrix/shared/types.ts @@ -51,6 +51,13 @@ export interface CustomColumnOptions { width?: number; } +export interface DataColumnsConfig { + timeSeriesColumns: boolean; + width?: number; + count: number; + customColumns?: string[] | readonly string[]; +} + export interface FormatGridNumberOptions { value?: number; maxDecimals?: number; @@ -58,11 +65,16 @@ export interface FormatGridNumberOptions { export interface EnhancedGridColumn extends BaseGridColumn { id: string; + title: string; width?: number; type: ColumnType; editable: boolean; } +export type ResultColumn = Omit & { + title: string[]; +}; + export type AggregateConfig = AggregateType[] | boolean | "stats" | "all"; export interface MatrixAggregates { @@ -79,6 +91,12 @@ export interface MatrixDataDTO { index: number[]; } +export interface ResultMatrixDTO { + data: number[][]; + columns: string[][]; + index: string[]; +} + export type Coordinates = [number, number]; // Shape of updates provided by Glide Data Grid diff --git a/webapp/src/components/common/Matrix/shared/utils.ts b/webapp/src/components/common/Matrix/shared/utils.ts index 0938519418..b9233c11a7 100644 --- a/webapp/src/components/common/Matrix/shared/utils.ts +++ b/webapp/src/components/common/Matrix/shared/utils.ts @@ -21,6 +21,8 @@ import { type AggregateConfig, type DateTimeMetadataDTO, type FormatGridNumberOptions, + DataColumnsConfig, + ResultColumn, } from "./types"; import { parseISO, Locale } from "date-fns"; import { fr, enUS } from "date-fns/locale"; @@ -169,14 +171,18 @@ export function generateTimeSeriesColumns({ * * @param customColumns - An array of strings representing the custom column titles. * @param customColumns.titles - The titles of the custom columns. + * @param customColumns.width - The width of each column. + * @returns An array of EnhancedGridColumn objects representing the generated custom columns. */ export function generateCustomColumns({ titles, + width, }: CustomColumnOptions): EnhancedGridColumn[] { return titles.map((title, index) => ({ id: `custom${index + 1}`, title, + width, type: Column.Number, editable: true, })); @@ -185,26 +191,33 @@ export function generateCustomColumns({ /** * Generates an array of data columns for a matrix grid. * - * @param timeSeriesColumns - A boolean indicating whether to enable time series columns. - * @param columnCount - The number of columns to generate. - * @param customColumns - An optional array of custom column titles. - * @param colWidth - The width of each column. - * @returns An array of EnhancedGridColumn objects representing the generated data columns. + * @param config - Configuration object for generating columns + * @param config.timeSeriesColumns - A boolean indicating whether to enable time series columns + * @param config.count - The number of columns to generate + * @param config.customColumns - An optional array of custom column titles + * @param config.width - The width of each column + * + * @returns An array of EnhancedGridColumn objects representing the generated data columns */ -export function generateDataColumns( - timeSeriesColumns: boolean, - columnCount: number, - customColumns?: string[] | readonly string[], - colWidth?: number, -): EnhancedGridColumn[] { +export function generateDataColumns({ + timeSeriesColumns, + width, + count, + customColumns, +}: DataColumnsConfig): EnhancedGridColumn[] { // If custom columns are provided, use them if (customColumns) { - return generateCustomColumns({ titles: customColumns, width: colWidth }); + return generateCustomColumns({ + titles: customColumns, + width, + }); } // Else, generate time series columns if enabled if (timeSeriesColumns) { - return generateTimeSeriesColumns({ count: columnCount }); + return generateTimeSeriesColumns({ + count, + }); } return []; @@ -318,46 +331,68 @@ export function calculateMatrixAggregates( * // Both columns will be grouped under "OV. COST (Euro)" * ``` */ + export function groupResultColumns( - columns: EnhancedGridColumn[], + columns: Array, ): EnhancedGridColumn[] { - return columns.map((column) => { - try { - const titles = Array.isArray(column.title) - ? column.title - : [String(column.title)]; - - // Extract and validate components - // [0]: Variable name (e.g., "OV. COST") - // [1]: Unit (e.g., "Euro") - // [2]: Statistic type (e.g., "MIN", "MAX", "STD") - const [variable, unit, stat] = titles.map((t) => String(t).trim()); - - // Create group name: - // - If unit exists and is not empty/whitespace, add it in parentheses - // - If no unit or empty unit, use variable name alone - const hasUnit = unit && unit.trim().length > 0; - const title = hasUnit ? `${variable} (${unit})` : variable; - - // If no stats, it does not make sense to group columns - if (!stat) { - return { - ...column, - title, - }; - } - + return columns.map((column): EnhancedGridColumn => { + const titles = Array.isArray(column.title) + ? column.title + : [String(column.title)]; + + // Extract and validate components + // [0]: Variable name (e.g., "OV. COST") + // [1]: Unit (e.g., "Euro") + // [2]: Statistic type (e.g., "MIN", "MAX", "STD") + const [variable, unit, stat] = titles.map((t) => String(t).trim()); + + // Create group name: + // - If unit exists and is not empty/whitespace, add it in parentheses + // - If no unit or empty unit, use variable name alone + const hasUnit = unit && unit.trim().length > 0; + const title = hasUnit ? `${variable} (${unit})` : variable; + + // If no stats, it does not make sense to group columns + if (!stat) { return { ...column, - group: title, // Group header title - title: stat.toLowerCase(), // Sub columns title - themeOverride: { - bgHeader: "#2D2E40", // Sub columns bg color - }, + title, }; - } catch (error) { - console.error(`Error processing column ${column.id}:`, error); - return column; } + + return { + ...column, + group: title, // Group header title + title: stat.toLowerCase(), // Sub columns title, + + themeOverride: { + bgHeader: "#2D2E40", // Sub columns bg color + }, + }; }); } + +/** + * Generates an array of ResultColumn objects from a 2D array of column titles. + * Each title array should follow the format [variable, unit, stat] as used in result matrices. + + * This function is designed to work in conjunction with groupResultColumns() + * to create properly formatted and grouped result matrix columns. + * + * @param titles - 2D array of string arrays, where each inner array contains: + * - [0]: Variable name (e.g., "OV. COST") + * - [1]: Unit (e.g., "Euro", "MW") + * - [2]: Statistic type (e.g., "MIN", "MAX", "STD") + * + * @returns Array of ResultColumn objects ready for use in result matrices + * + * @see groupResultColumns - Use this function to apply grouping to the generated columns + */ +export function generateResultColumns(titles: string[][]): ResultColumn[] { + return titles.map((title, index) => ({ + id: `custom${index + 1}`, + title: title, + type: Column.Number, + editable: false, + })); +} diff --git a/webapp/src/components/common/Matrix/styles.ts b/webapp/src/components/common/Matrix/styles.ts index 7711a73c8e..2b6686679f 100644 --- a/webapp/src/components/common/Matrix/styles.ts +++ b/webapp/src/components/common/Matrix/styles.ts @@ -13,6 +13,7 @@ */ import { Box, styled, Typography } from "@mui/material"; +import { Theme } from "@glideapps/glide-data-grid"; export const MatrixContainer = styled(Box)(() => ({ width: "100%", @@ -36,3 +37,61 @@ export const MatrixTitle = styled(Typography)(() => ({ fontWeight: 400, lineHeight: 1, })); + +export const darkTheme: Theme = { + accentColor: "rgba(255, 184, 0, 0.9)", + accentLight: "rgba(255, 184, 0, 0.2)", + accentFg: "#FFFFFF", + textDark: "#FFFFFF", + textMedium: "#C1C3D9", + textLight: "#A1A5B9", + textBubble: "#FFFFFF", + bgIconHeader: "#1E1F2E", + fgIconHeader: "#FFFFFF", + textHeader: "#FFFFFF", + textGroupHeader: "#C1C3D9", + bgCell: "#262737", // main background color + bgCellMedium: "#2E2F42", + bgHeader: "#1E1F2E", + bgHeaderHasFocus: "#2E2F42", + bgHeaderHovered: "#333447", + bgBubble: "#333447", + bgBubbleSelected: "#3C3E57", + bgSearchResult: "#6366F133", + borderColor: "rgba(255, 255, 255, 0.12)", + drilldownBorder: "rgba(255, 255, 255, 0.35)", + linkColor: "#818CF8", + headerFontStyle: "bold 11px", + baseFontStyle: "13px", + fontFamily: "Inter, sans-serif", + editorFontSize: "13px", + lineHeight: 1.5, + textHeaderSelected: "#FFFFFF", + cellHorizontalPadding: 8, + cellVerticalPadding: 5, + headerIconSize: 16, + markerFontStyle: "normal", +}; + +export const readOnlyDarkTheme: Partial = { + bgCell: "#1A1C2A", + bgCellMedium: "#22243A", + textDark: "#FAF9F6", + textMedium: "#808080", + textLight: "#606060", + accentColor: "#4A4C66", + accentLight: "rgba(74, 76, 102, 0.2)", + borderColor: "rgba(255, 255, 255, 0.08)", + drilldownBorder: "rgba(255, 255, 255, 0.2)", + headerFontStyle: "bold 11px", +}; + +export const aggregatesTheme: Partial = { + bgCell: "#3D3E5F", + bgCellMedium: "#383A5C", + textDark: "#FFFFFF", + fontFamily: "Inter, sans-serif", + baseFontStyle: "13px", + editorFontSize: "13px", + headerFontStyle: "bold 11px", +}; diff --git a/webapp/src/components/common/dialogs/DataViewerDialog/index.tsx b/webapp/src/components/common/dialogs/DataViewerDialog/index.tsx index 156cfbe3e2..5999492f43 100644 --- a/webapp/src/components/common/dialogs/DataViewerDialog/index.tsx +++ b/webapp/src/components/common/dialogs/DataViewerDialog/index.tsx @@ -13,114 +13,67 @@ */ import { useTranslation } from "react-i18next"; -import { Box, IconButton, Tooltip, Typography } from "@mui/material"; -import { useSnackbar } from "notistack"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import { Box, Typography } from "@mui/material"; import { Code } from "./styles"; -import { MatrixType } from "../../../../common/types"; -import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; import OkDialog from "../OkDialog"; -import EditableMatrix from "../../EditableMatrix"; -import { getStudyMatrixIndex } from "../../../../services/api/matrix"; import SimpleLoader from "../../loaders/SimpleLoader"; - -type MatrixTypeWithId = MatrixType & { id?: string }; +import MatrixGrid from "@/components/common/Matrix/components/MatrixGrid"; +import { generateDataColumns } from "@/components/common/Matrix/shared/utils"; +import { MatrixDataDTO } from "@/components/common/Matrix/shared/types"; interface Props { - studyId?: string; filename: string; - content?: string | MatrixTypeWithId; + content?: string | MatrixDataDTO; loading?: boolean; onClose: () => void; isMatrix?: boolean; - readOnly?: boolean; } -function DataViewerDialog(props: Props) { - const [t] = useTranslation(); - const { enqueueSnackbar } = useSnackbar(); - const { - studyId, - filename, - content, - onClose, - isMatrix, - loading, - readOnly = true, - } = props; - - const { data: matrixIndex } = usePromiseWithSnackbarError( - async () => { - if (studyId) { - return getStudyMatrixIndex(studyId); - } - return undefined; - }, - { - errorMessage: t("matrix.error.failedToRetrieveIndex"), - deps: [studyId], - }, +function isMatrixData( + content: string | MatrixDataDTO, +): content is MatrixDataDTO { + return ( + typeof content === "object" && "data" in content && "columns" in content ); +} - //////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////// - - const copyId = async (matrixId: string): Promise => { - try { - await navigator.clipboard.writeText(matrixId); - enqueueSnackbar(t("data.success.matrixIdCopied"), { - variant: "success", - }); - } catch (e) { - enqueueSnackbar(t("data.error.copyMatrixId"), { variant: "error" }); - } - }; +/** + * @deprecated This component is legacy and only used in Xpansion views. + * TODO: This component should be removed when the Xpansion views are reworked. + * The new implementation should separate the following responsibilities: + * - Matrix data visualization + * - Text content display + * + * @param props - Component props + * @param props.filename - The name of the file to be displayed + * @param [props.content] - The content to be displayed, either text or matrix data + * @param props.onClose - Callback function to handle dialog close + * @param [props.isMatrix] - Flag indicating if the content is matrix data + * @param [props.loading] - Flag indicating if the content is being loaded + * @returns The rendered DataViewerDialog component + */ +function DataViewerDialog({ + filename, + content, + onClose, + isMatrix, + loading, +}: Props) { + const [t] = useTranslation(); //////////////////////////////////////////////////////////////// // JSX //////////////////////////////////////////////////////////////// - const renderContent = (data: MatrixTypeWithId | string) => - isMatrix ? ( - - - - ) : ( - - {data as string} - - ); - return ( {`Matrix - ${filename}`} - {content && (content as MatrixTypeWithId).id && ( - - copyId((content as MatrixTypeWithId).id as string) - } - sx={{ - ml: 1, - color: "action.active", - }} - > - - - - - )}
) : ( filename @@ -134,8 +87,27 @@ function DataViewerDialog(props: Props) { okButtonText={t("global.close")} onOk={onClose} > - {!!loading && !content && } - {!!content && renderContent(content)} + {loading && !content && } + {content && ( + <> + {isMatrix && isMatrixData(content) && ( + + )} + {!isMatrix && typeof content === "string" && ( + + {content} + + )} + + )} ); } diff --git a/webapp/src/components/common/dialogs/DatabaseUploadDialog/components/MatrixContent.tsx b/webapp/src/components/common/dialogs/DatabaseUploadDialog/components/MatrixContent.tsx new file mode 100644 index 0000000000..3856e72690 --- /dev/null +++ b/webapp/src/components/common/dialogs/DatabaseUploadDialog/components/MatrixContent.tsx @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2024, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Box, Divider, Typography } from "@mui/material"; +import { MatrixInfoDTO, MatrixDTO } from "@/common/types"; +import MatrixGrid from "@/components/common/Matrix/components/MatrixGrid"; +import ButtonBack from "@/components/common/ButtonBack"; +import { getMatrix } from "@/services/api/matrix"; +import usePromiseWithSnackbarError from "@/hooks/usePromiseWithSnackbarError"; +import { generateDataColumns } from "@/components/common/Matrix/shared/utils"; +import EmptyView from "@/components/common/page/SimpleContent"; +import { GridOff } from "@mui/icons-material"; + +interface MatrixContentProps { + matrix: MatrixInfoDTO; + onBack: () => void; +} + +function MatrixContent({ matrix, onBack }: MatrixContentProps) { + const { t } = useTranslation(); + + const { data: matrixData } = usePromiseWithSnackbarError( + () => getMatrix(matrix.id), + { + errorMessage: t("data.error.matrix"), + }, + ); + + const matrixColumns = useMemo( + () => + matrixData + ? generateDataColumns({ + timeSeriesColumns: true, + count: matrixData.columns.length, + }) + : [], + [matrixData], + ); + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + if (!matrixData) { + return null; + } + + return ( + <> + + + {matrix.name} + + + {!matrixData.data[0]?.length ? ( + + ) : ( + + )} + + ); +} + +export default MatrixContent; diff --git a/webapp/src/components/common/dialogs/DatabaseUploadDialog/index.tsx b/webapp/src/components/common/dialogs/DatabaseUploadDialog/index.tsx new file mode 100644 index 0000000000..8ef68ba833 --- /dev/null +++ b/webapp/src/components/common/dialogs/DatabaseUploadDialog/index.tsx @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2024, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSnackbar } from "notistack"; +import { Box, Button } from "@mui/material"; +import { StudyMetadata } from "@/common/types"; +import { CommandEnum } from "@/components/App/Singlestudy/Commands/Edition/commandTypes"; +import BasicDialog from "@/components/common/dialogs/BasicDialog"; +import DataPropsView from "@/components/App/Data/DataPropsView"; +import FileTable from "@/components/common/FileTable"; +import SplitView from "@/components/common/SplitView"; +import { getMatrixList } from "@/services/api/matrix"; +import { appendCommands } from "@/services/api/variant"; +import usePromiseWithSnackbarError from "@/hooks/usePromiseWithSnackbarError"; +import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; +import MatrixContent from "./components/MatrixContent"; +import { toError } from "@/utils/fnUtils"; +import SimpleLoader from "@/components/common/loaders/SimpleLoader"; + +interface DatabaseUploadDialogProps { + studyId: StudyMetadata["id"]; + path: string; + open: boolean; + onClose: () => void; +} + +function DatabaseUploadDialog({ + studyId, + path, + open, + onClose, +}: DatabaseUploadDialogProps) { + const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); + const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); + const [isUploading, setIsUploading] = useState(false); + const [selectedItem, setSelectedItem] = useState(""); + const [matrixId, setMatrixId] = useState(); + + const { data: matrices } = usePromiseWithSnackbarError(getMatrixList, { + errorMessage: t("data.error.matrixList"), + }); + + const selectedMatrix = useMemo( + () => matrices?.find((item) => item.id === selectedItem)?.matrices || [], + [matrices, selectedItem], + ); + + const matrix = useMemo(() => { + if (!matrixId || !selectedMatrix) { + return undefined; + } + + const matrix = selectedMatrix.find((m) => m.id === matrixId); + return matrix ? { id: matrix.id, name: matrix.name } : undefined; + }, [matrixId, selectedMatrix]); + + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + const handleMatrixClick = (id: string) => { + setMatrixId(id); + }; + + const handleUpload = async (matrixId: string) => { + setIsUploading(true); + + try { + await appendCommands(studyId, [ + { + action: CommandEnum.REPLACE_MATRIX, + args: { target: path, matrix: matrixId }, + }, + ]); + + enqueueSnackbar(t("data.success.matrixAssignation"), { + variant: "success", + }); + + onClose(); + } catch (err) { + enqueueErrorSnackbar(t("data.error.matrixAssignation"), toError(err)); + } finally { + setIsUploading(false); + } + }; + + const handleClose = ( + _event: Record, + reason: "backdropClick" | "escapeKeyDown", + ) => { + if (!isUploading) { + onClose(); + } + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ( + + {t("global.close")} + + } + maxWidth="xl" + fullWidth + contentProps={{ + sx: { p: 1, height: "95vh", width: 1 }, + }} + > + {isUploading ? ( + + ) : ( + + + + {selectedItem && + (matrix ? ( + setMatrixId(undefined)} + /> + ) : ( + + ))} + + + )} + + ); +} + +export default DatabaseUploadDialog; diff --git a/webapp/src/components/common/dialogs/DigestDialog.tsx b/webapp/src/components/common/dialogs/DigestDialog.tsx index def48a8162..db169ecaf1 100644 --- a/webapp/src/components/common/dialogs/DigestDialog.tsx +++ b/webapp/src/components/common/dialogs/DigestDialog.tsx @@ -14,7 +14,6 @@ import { Skeleton } from "@mui/material"; import OkDialog, { OkDialogProps } from "./OkDialog"; -import EditableMatrix from "../EditableMatrix"; import UsePromiseCond from "../utils/UsePromiseCond"; import type { LaunchJob } from "../../../common/types"; import { getStudyData } from "../../../services/api/study"; @@ -23,8 +22,8 @@ import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; import EmptyView from "../page/SimpleContent"; import SearchOffIcon from "@mui/icons-material/SearchOff"; - -// TODO: redesign DataViewerDialog to use path, then remove this component +import { generateDataColumns } from "@/components/common/Matrix/shared/utils"; +import { MatrixGridSynthesis } from "@/components/common/Matrix/components/MatrixGridSynthesis"; export interface DigestDialogProps extends Pick { @@ -71,11 +70,14 @@ function DigestDialog({ }} ifFulfilled={(matrix) => matrix && ( - ) } diff --git a/webapp/src/components/common/dialogs/ImportDialog.tsx b/webapp/src/components/common/dialogs/UploadDialog.tsx similarity index 86% rename from webapp/src/components/common/dialogs/ImportDialog.tsx rename to webapp/src/components/common/dialogs/UploadDialog.tsx index a16bbf6df6..c0dbf3d59c 100644 --- a/webapp/src/components/common/dialogs/ImportDialog.tsx +++ b/webapp/src/components/common/dialogs/UploadDialog.tsx @@ -24,7 +24,7 @@ import { enqueueSnackbar } from "notistack"; import { PromiseAny } from "../../../utils/tsUtils"; import FileDownloadIcon from "@mui/icons-material/FileDownload"; -interface ImportDialogProps extends Omit { +interface UploadDialogProps extends Omit { dropzoneText?: string; accept?: Accept; onCancel: VoidFunction; @@ -34,7 +34,7 @@ interface ImportDialogProps extends Omit { ) => PromiseAny; } -function ImportDialog(props: ImportDialogProps) { +function UploadDialog(props: UploadDialogProps) { const { dropzoneText, accept, @@ -59,18 +59,19 @@ function ImportDialog(props: ImportDialogProps) { }); useEffect(() => { - if (isUploading) { - const listener = (e: BeforeUnloadEvent) => { - // eslint-disable-next-line no-param-reassign - e.returnValue = "Import"; - }; - - window.addEventListener("beforeunload", listener); + // Protect against data loss by preventing navigation/refresh during file upload + // This displays a browser warning when trying to: + // - Close the browser tab/window + // - Refresh the page + // - Navigate away from the page + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (isUploading) { + e.preventDefault(); + } + }; - return () => { - window.removeEventListener("beforeunload", listener); - }; - } + window.addEventListener("beforeunload", handleBeforeUnload); + return () => window.removeEventListener("beforeunload", handleBeforeUnload); }, [isUploading]); //////////////////////////////////////////////////////////////// @@ -102,7 +103,7 @@ function ImportDialog(props: ImportDialogProps) { setInvalidText(fileRejections[0].errors[0].message); } - const handleClose: ImportDialogProps["onClose"] = (...args) => { + const handleClose: UploadDialogProps["onClose"] = (...args) => { if (!isUploading) { onCancel(); onClose?.(...args); @@ -174,4 +175,4 @@ function ImportDialog(props: ImportDialogProps) { ); } -export default ImportDialog; +export default UploadDialog; diff --git a/webapp/src/hooks/useDebouncedField.tsx b/webapp/src/hooks/useDebouncedField.tsx new file mode 100644 index 0000000000..ea7dce4a1e --- /dev/null +++ b/webapp/src/hooks/useDebouncedField.tsx @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2024, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import { useState, useEffect, useCallback } from "react"; +import useDebounce from "./useDebounce"; + +interface UseDebouncedFieldOptions { + value: T; + onChange: (value: T) => void; + delay: number; + transformValue?: (value: T) => T; +} + +interface UseDebouncedFieldResult { + localValue: T; + handleChange: (value: T) => void; + setLocalValue: (value: T) => void; +} + +/** + * A hook that implements a "locally controlled, parentally debounced" pattern for form fields. + * + * 1. Controlled by Parent: + * - Parent owns the source of truth + * - Hook syncs with parent value changes + * + * 2. Local State Benefits: + * - Immediate UI feedback during typing + * - No input lag + * + * 3. Debounced Updates: + * - Parent updates (e.g. API calls) only trigger after user stops typing + * - Prevents excessive updates during rapid changes + * + * @param options - Configuration object for the debounced field + * @param options.value - The controlled value from parent + * @param options.onChange - Callback to update parent value (debounced) + * @param options.delay - Debounce delay in milliseconds (default: 500) + * @param options.transformValue - Optional value transformation function + + * @returns Object containing: + * - localValue: The immediate local state value + * - handleChange: Function to handle value changes (with debouncing) + * - setLocalValue: Function to directly update local value without debouncing + * + * @example + * ```tsx + * // Example with API calls + * function SearchField({ onSearch }) { + * const { localValue, handleChange } = useDebouncedField({ + * value: searchTerm, // Parent control + * onChange: onSearch, // Debounced API call + * delay: 500 // Wait 500ms after typing stops + * }); + * + * return ( + * + * ); + * } + * ``` + */ +export function useDebouncedField({ + value, + onChange, + delay = 500, + transformValue, +}: UseDebouncedFieldOptions): UseDebouncedFieldResult { + const [localValue, setLocalValue] = useState(value); + + // Sync local value with prop changes + useEffect(() => { + setLocalValue(value); + }, [value]); + + const debouncedOnChange = useDebounce(onChange, delay); + + const handleChange = useCallback( + (newValue: T) => { + const value = transformValue?.(newValue) ?? newValue; + setLocalValue(value); + debouncedOnChange(value); + }, + [debouncedOnChange, transformValue], + ); + + return { + localValue, + handleChange, + setLocalValue, + }; +} diff --git a/webapp/src/services/api/xpansion.ts b/webapp/src/services/api/xpansion.ts index 2746c90c2e..3b936177d9 100644 --- a/webapp/src/services/api/xpansion.ts +++ b/webapp/src/services/api/xpansion.ts @@ -13,12 +13,12 @@ */ import { AxiosRequestConfig } from "axios"; -import { MatrixType } from "../../common/types"; import { XpansionCandidate, XpansionSettings, } from "../../components/App/Singlestudy/explore/Xpansion/types"; import client from "./client"; +import { MatrixDataDTO } from "@/components/common/Matrix/shared/types"; export const createXpansionConfiguration = async ( uuid: string, @@ -248,7 +248,7 @@ export const deleteCapacity = async ( export const getCapacity = async ( uuid: string, filename: string, -): Promise => { +): Promise => { const res = await client.get( `/v1/studies/${uuid}/extensions/xpansion/resources/capacities/${filename}`, );