diff --git a/Dockerfile b/Dockerfile index ee2825af17..dccc2ff4fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM python:3.8-slim-buster +#FROM python:3.8-slim-buster +FROM brunneis/python:3.8.3-ubuntu-20.04 ENV ANTAREST_CONF /resources/application.yaml diff --git a/README.md b/README.md index 3d6df51e0c..a9a3db1298 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ -# AntaREST Storage +# Antares Web [![CI](https://github.com/AntaresSimulatorTeam/AntaREST/workflows/main/badge.svg)](https://github.com/AntaresSimulatorTeam/AntaREST/actions?query=workflow%3Amain) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=AntaresSimulatorTeam_api-iso-antares&metric=coverage)](https://sonarcloud.io/dashboard?id=AntaresSimulatorTeam_api-iso-antares) [![Licence](https://img.shields.io/github/license/AntaresSimulatorTeam/AntaREST)](https://www.apache.org/licenses/LICENSE-2.0) - +![Screenshot](./docs/assets/media/img/readme_screenshot.png) + +## Documentation + +The full project documentation can be found in the [readthedocs website](https://antares-web.readthedocs.io/en/latest). ## Build the API diff --git a/alembic/versions/9846e90c2868_fix_bot_foreign_key.py b/alembic/versions/9846e90c2868_fix_bot_foreign_key.py index 9fa93fc913..c79b428405 100644 --- a/alembic/versions/9846e90c2868_fix_bot_foreign_key.py +++ b/alembic/versions/9846e90c2868_fix_bot_foreign_key.py @@ -5,7 +5,6 @@ Create Date: 2021-11-19 11:58:11.378519 """ -from sqlite3 import Connection from alembic import op import sqlalchemy as sa @@ -13,6 +12,7 @@ # revision identifiers, used by Alembic. from sqlalchemy import text +from sqlalchemy.engine import Connection from antarest.login.model import Bot, User diff --git a/antarest/__init__.py b/antarest/__init__.py index a87a359d4c..3736a3ba3b 100644 --- a/antarest/__init__.py +++ b/antarest/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.5.0" +__version__ = "2.5.1" from pathlib import Path diff --git a/antarest/core/config.py b/antarest/core/config.py index 2e3107b99e..49e29a0457 100644 --- a/antarest/core/config.py +++ b/antarest/core/config.py @@ -247,10 +247,15 @@ class RedisConfig: host: str = "localhost" port: int = 6379 + password: Optional[str] = None @staticmethod def from_dict(data: JSON) -> "RedisConfig": - return RedisConfig(host=data["host"], port=data["port"]) + return RedisConfig( + host=data["host"], + port=data["port"], + password=data.get("password", None), + ) @dataclass(frozen=True) diff --git a/antarest/core/exceptions.py b/antarest/core/exceptions.py index 83fb6da848..a65e882e4c 100644 --- a/antarest/core/exceptions.py +++ b/antarest/core/exceptions.py @@ -126,5 +126,10 @@ def __init__(self, message: str) -> None: super().__init__(HTTPStatus.NOT_FOUND, message) +class WritingInsideZippedFileException(HTTPException): + def __init__(self, message: str) -> None: + super().__init__(HTTPStatus.BAD_REQUEST, message) + + class StudyOutputNotFoundError(Exception): pass diff --git a/antarest/core/interfaces/eventbus.py b/antarest/core/interfaces/eventbus.py index 0c6af3063f..4c751ae234 100644 --- a/antarest/core/interfaces/eventbus.py +++ b/antarest/core/interfaces/eventbus.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from enum import Enum from typing import Any, Callable, Optional, List, Awaitable from pydantic import BaseModel @@ -6,7 +7,8 @@ from antarest.core.model import PermissionInfo -class EventType: +class EventType(str, Enum): + ANY = "_ANY" STUDY_CREATED = "STUDY_CREATED" STUDY_DELETED = "STUDY_DELETED" STUDY_EDITED = "STUDY_EDITED" @@ -31,6 +33,9 @@ class EventType: DOWNLOAD_FAILED = "DOWNLOAD_FAILED" MESSAGE_INFO = "MESSAGE_INFO" MAINTENANCE_MODE = "MAINTENANCE_MODE" + WORKER_TASK = "WORKER_TASK" + WORKER_TASK_STARTED = "WORKER_TASK_STARTED" + WORKER_TASK_ENDED = "WORKER_TASK_ENDED" class EventChannelDirectory: @@ -41,7 +46,7 @@ class EventChannelDirectory: class Event(BaseModel): - type: str + type: EventType payload: Any permissions: PermissionInfo = PermissionInfo() channel: Optional[str] = None @@ -52,11 +57,25 @@ class IEventBus(ABC): def push(self, event: Event) -> None: pass + @abstractmethod + def queue(self, event: Event, queue: str) -> None: + pass + + @abstractmethod + def add_queue_consumer( + self, listener: Callable[[Event], Awaitable[None]], queue: str + ) -> str: + pass + + @abstractmethod + def remove_queue_consumer(self, listener_id: str) -> None: + pass + @abstractmethod def add_listener( self, listener: Callable[[Event], Awaitable[None]], - type_filter: Optional[List[str]] = None, + type_filter: Optional[List[EventType]] = None, ) -> str: """ Add an event listener listener @@ -77,6 +96,20 @@ def start(self, threaded: bool = True) -> None: class DummyEventBusService(IEventBus): + def queue(self, event: Event, queue: str) -> None: + # Noop + pass + + def add_queue_consumer( + self, listener: Callable[[Event], Awaitable[None]], queue: str + ) -> str: + # Noop + pass + + def remove_queue_consumer(self, listener_id: str) -> None: + # Noop + pass + def push(self, event: Event) -> None: # Noop pass @@ -84,7 +117,7 @@ def push(self, event: Event) -> None: def add_listener( self, listener: Callable[[Event], Awaitable[None]], - type_filter: Optional[List[str]] = None, + type_filter: Optional[List[EventType]] = None, ) -> str: return "" diff --git a/antarest/core/tasks/model.py b/antarest/core/tasks/model.py index 7af6b49295..e8e7cf2e0b 100644 --- a/antarest/core/tasks/model.py +++ b/antarest/core/tasks/model.py @@ -17,6 +17,7 @@ class TaskType(str, Enum): ARCHIVE = "ARCHIVE" UNARCHIVE = "UNARCHIVE" SCAN = "SCAN" + WORKER_TASK = "WORKER_TASK" class TaskStatus(Enum): diff --git a/antarest/core/tasks/service.py b/antarest/core/tasks/service.py index 23cc552520..a3697011ce 100644 --- a/antarest/core/tasks/service.py +++ b/antarest/core/tasks/service.py @@ -6,7 +6,7 @@ from concurrent.futures import ThreadPoolExecutor, Future from enum import Enum from http import HTTPStatus -from typing import Callable, Optional, List, Dict, Awaitable +from typing import Callable, Optional, List, Dict, Awaitable, Union, cast from fastapi import HTTPException @@ -38,6 +38,7 @@ from antarest.core.tasks.repository import TaskJobRepository from antarest.core.utils.fastapi_sqlalchemy import db from antarest.core.utils.utils import retry +from antarest.worker.worker import WorkerTaskCommand, WorkerTaskResult logger = logging.getLogger(__name__) @@ -46,6 +47,17 @@ class ITaskService(ABC): + @abstractmethod + def add_worker_task( + self, + task_type: str, + task_args: Dict[str, Union[int, float, bool, str]], + name: Optional[str], + ref_id: Optional[str], + request_params: RequestParameters, + ) -> str: + raise NotImplementedError() + @abstractmethod def add_task( self, @@ -101,10 +113,72 @@ def __init__( self.threadpool = ThreadPoolExecutor( max_workers=config.tasks.max_workers, thread_name_prefix="taskjob_" ) - self.event_bus.add_listener(self.create_task_event_callback()) + self.event_bus.add_listener( + self.create_task_event_callback(), [EventType.TASK_CANCEL_REQUEST] + ) # set the status of previously running job to FAILED due to server restart self._fix_running_status() + def _create_worker_task( + self, + task_id: str, + task_type: str, + task_args: Dict[str, Union[int, float, bool, str]], + ) -> Callable[[TaskUpdateNotifier], TaskResult]: + task_result_wrapper: List[TaskResult] = [] + + def _create_awaiter( + res_wrapper: List[TaskResult], + ) -> Callable[[Event], Awaitable[None]]: + async def _await_task_end(event: Event) -> None: + task_event = cast(WorkerTaskResult, event.payload) + if task_event.task_id == task_id: + res_wrapper.append(task_event.task_result) + + return _await_task_end + + def _send_worker_task(logger: TaskUpdateNotifier) -> TaskResult: + listener_id = self.event_bus.add_listener( + _create_awaiter(task_result_wrapper), + [EventType.WORKER_TASK_ENDED], + ) + self.event_bus.queue( + Event( + type=EventType.WORKER_TASK, + payload=WorkerTaskCommand( + task_id=task_id, + task_type=task_type, + task_args=task_args, + ), + ), + task_type, + ) + while not task_result_wrapper: + time.sleep(1) + self.event_bus.remove_listener(listener_id) + return task_result_wrapper[0] + + return _send_worker_task + + def add_worker_task( + self, + task_type: str, + task_args: Dict[str, Union[int, float, bool, str]], + name: Optional[str], + ref_id: Optional[str], + request_params: RequestParameters, + ) -> str: + task = self._create_task( + name, TaskType.WORKER_TASK, ref_id, request_params + ) + self._launch_task( + self._create_worker_task(str(task.id), task_type, task_args), + task, + None, + request_params, + ) + return str(task.id) + def add_task( self, action: Task, @@ -114,10 +188,21 @@ def add_task( custom_event_messages: Optional[CustomTaskEventMessages], request_params: RequestParameters, ) -> str: + task = self._create_task(name, task_type, ref_id, request_params) + self._launch_task(action, task, custom_event_messages, request_params) + return str(task.id) + + def _create_task( + self, + name: Optional[str], + task_type: Optional[TaskType], + ref_id: Optional[str], + request_params: RequestParameters, + ) -> TaskJob: if not request_params.user: raise MustBeAuthenticatedError() - task = self.repo.save( + return self.repo.save( TaskJob( name=name or "Unnamed", owner_id=request_params.user.impersonator, @@ -126,6 +211,16 @@ def add_task( ) ) + def _launch_task( + self, + action: Task, + task: TaskJob, + custom_event_messages: Optional[CustomTaskEventMessages], + request_params: RequestParameters, + ) -> None: + if not request_params.user: + raise MustBeAuthenticatedError() + self.event_bus.push( Event( type=EventType.TASK_ADDED, @@ -144,12 +239,10 @@ def add_task( self._run_task, action, task.id, custom_event_messages ) self.tasks[task.id] = future - return str(task.id) def create_task_event_callback(self) -> Callable[[Event], Awaitable[None]]: async def task_event_callback(event: Event) -> None: - if event.type == EventType.TASK_CANCEL_REQUEST: - self._cancel_task(str(event.payload), dispatch=False) + self._cancel_task(str(event.payload), dispatch=False) return task_event_callback @@ -227,13 +320,14 @@ def await_task( ) end = time.time() + (timeout_sec or DEFAULT_AWAIT_MAX_TIMEOUT) while time.time() < end: - task = self.repo.get(task_id) - if not task: - logger.error(f"Awaited task {task_id} was not found") - break - if TaskStatus(task.status).is_final(): - break - time.sleep(2) + with db(): + task = self.repo.get(task_id) + if not task: + logger.error(f"Awaited task {task_id} was not found") + break + if TaskStatus(task.status).is_final(): + break + time.sleep(2) def _run_task( self, diff --git a/antarest/core/utils/utils.py b/antarest/core/utils/utils.py index e4a876569c..c00c087989 100644 --- a/antarest/core/utils/utils.py +++ b/antarest/core/utils/utils.py @@ -3,7 +3,7 @@ import time from glob import escape from pathlib import Path -from typing import IO, Any, Optional, Callable, TypeVar, List +from typing import IO, Any, Optional, Callable, TypeVar, List, Union, Awaitable from zipfile import ( ZipFile, BadZipFile, @@ -90,7 +90,10 @@ def get_local_path() -> Path: def new_redis_instance(config: RedisConfig) -> redis.Redis: # type: ignore - return redis.Redis(host=config.host, port=config.port, db=0) + redis_client = redis.Redis( + host=config.host, port=config.port, password=config.password, db=0 + ) + return redis_client class StopWatch: @@ -164,3 +167,14 @@ def unzip( zipf.extractall(dir_path) if remove_source_zip: zip_path.unlink() + + +def suppress_exception( + callback: Callable[[], T], + logger: Callable[[Exception], None], +) -> Optional[T]: + try: + return callback() + except Exception as e: + logger(e) + return None diff --git a/antarest/eventbus/business/interfaces.py b/antarest/eventbus/business/interfaces.py index 6eb614b149..b180775c4f 100644 --- a/antarest/eventbus/business/interfaces.py +++ b/antarest/eventbus/business/interfaces.py @@ -1,18 +1,27 @@ +import abc from abc import abstractmethod -from typing import List +from typing import List, Optional from antarest.core.interfaces.eventbus import Event -class IEventBusBackend: +class IEventBusBackend(abc.ABC): @abstractmethod def push_event(self, event: Event) -> None: - pass + raise NotImplementedError + + @abstractmethod + def queue_event(self, event: Event, queue: str) -> None: + raise NotImplementedError + + @abstractmethod + def pull_queue(self, queue: str) -> Optional[Event]: + raise NotImplementedError @abstractmethod def get_events(self) -> List[Event]: - pass + raise NotImplementedError @abstractmethod def clear_events(self) -> None: - pass + raise NotImplementedError diff --git a/antarest/eventbus/business/local_eventbus.py b/antarest/eventbus/business/local_eventbus.py index e01af50d5e..4851a1ed51 100644 --- a/antarest/eventbus/business/local_eventbus.py +++ b/antarest/eventbus/business/local_eventbus.py @@ -1,5 +1,5 @@ import logging -from typing import List +from typing import List, Dict, Optional from antarest.core.interfaces.eventbus import Event from antarest.eventbus.business.interfaces import IEventBusBackend @@ -10,6 +10,7 @@ class LocalEventBus(IEventBusBackend): def __init__(self) -> None: self.events: List[Event] = [] + self.queues: Dict[str, List[Event]] = {} def push_event(self, event: Event) -> None: self.events.append(event) @@ -19,3 +20,13 @@ def get_events(self) -> List[Event]: def clear_events(self) -> None: self.events.clear() + + def queue_event(self, event: Event, queue: str) -> None: + if queue not in self.queues: + self.queues[queue] = [] + self.queues[queue].append(event) + + def pull_queue(self, queue: str) -> Optional[Event]: + if queue in self.queues and len(self.queues[queue]) > 0: + return self.queues[queue].pop(0) + return None diff --git a/antarest/eventbus/business/redis_eventbus.py b/antarest/eventbus/business/redis_eventbus.py index bdbfc8b317..4e27a7793c 100644 --- a/antarest/eventbus/business/redis_eventbus.py +++ b/antarest/eventbus/business/redis_eventbus.py @@ -1,7 +1,7 @@ import dataclasses import json import logging -from typing import List +from typing import List, Optional, cast from redis.client import Redis @@ -22,6 +22,15 @@ def __init__(self, redis_client: Redis) -> None: # type: ignore def push_event(self, event: Event) -> None: self.redis.publish(REDIS_STORE_KEY, event.json()) + def queue_event(self, event: Event, queue: str) -> None: + self.redis.rpush(queue, event.json()) + + def pull_queue(self, queue: str) -> Optional[Event]: + event = self.redis.lpop(queue) + if event: + return cast(Optional[Event], Event.parse_raw(event)) + return None + def get_events(self) -> List[Event]: try: event = self.pubsub.get_message(ignore_subscribe_messages=True) diff --git a/antarest/eventbus/service.py b/antarest/eventbus/service.py index b2a84af6dd..a7b8cfd2c7 100644 --- a/antarest/eventbus/service.py +++ b/antarest/eventbus/service.py @@ -1,11 +1,13 @@ import asyncio import logging +import random import threading import time -from typing import List, Callable, Optional, Dict, Awaitable +from typing import List, Callable, Optional, Dict, Awaitable, Any, cast from uuid import uuid4 from antarest.core.interfaces.eventbus import Event, IEventBus, EventType +from antarest.core.utils.utils import suppress_exception from antarest.eventbus.business.interfaces import IEventBusBackend logger = logging.getLogger(__name__) @@ -16,7 +18,13 @@ def __init__( self, backend: IEventBusBackend, autostart: bool = True ) -> None: self.backend = backend - self.listeners: Dict[str, Callable[[Event], Awaitable[None]]] = {} + self.listeners: Dict[ + EventType, Dict[str, Callable[[Event], Awaitable[None]]] + ] = {ev_type: {} for ev_type in EventType} + self.consumers: Dict[ + str, Dict[str, Callable[[Event], Awaitable[None]]] + ] = {} + self.lock = threading.Lock() if autostart: self.start() @@ -24,35 +32,89 @@ def __init__( def push(self, event: Event) -> None: self.backend.push_event(event) + def queue(self, event: Event, queue: str) -> None: + self.backend.queue_event(event, queue) + + def add_queue_consumer( + self, listener: Callable[[Event], Awaitable[None]], queue: str + ) -> str: + with self.lock: + listener_id = str(uuid4()) + if queue not in self.consumers: + self.consumers[queue] = {} + self.consumers[queue][listener_id] = listener + return listener_id + + def remove_queue_consumer(self, listener_id: str) -> None: + with self.lock: + for queue in self.consumers: + if listener_id in self.consumers[queue]: + del self.consumers[queue][listener_id] + def add_listener( self, listener: Callable[[Event], Awaitable[None]], - type_filter: Optional[List[str]] = None, + type_filter: Optional[List[EventType]] = None, ) -> str: with self.lock: listener_id = str(uuid4()) - self.listeners[listener_id] = listener + types = type_filter or [EventType.ANY] + for listener_type in types: + self.listeners[listener_type][listener_id] = listener return listener_id def remove_listener(self, listener_id: str) -> None: with self.lock: - del self.listeners[listener_id] + for listener_type in self.listeners: + if listener_id in self.listeners[listener_type]: + del self.listeners[listener_type][listener_id] async def _run_loop(self) -> None: while True: time.sleep(0.2) - await self._on_events() + try: + await self._on_events() + except Exception as e: + logger.error( + f"Unexpected error when processing events", exc_info=e + ) async def _on_events(self) -> None: with self.lock: + for queue in self.consumers: + if len(self.consumers[queue]) > 0: + event = self.backend.pull_queue(queue) + while event is not None: + try: + await list(self.consumers[queue].values())[ + random.randint( + 0, len(self.consumers[queue]) - 1 + ) + ](event) + except Exception as ex: + logger.error( + f"Failed to process queue event {event.type}", + exc_info=ex, + ) + event = self.backend.pull_queue(queue) + for e in self.backend.get_events(): - for listener in self.listeners.values(): - try: - await listener(e) - except Exception as ex: - logger.error( - f"Failed to process event {e.type}", exc_info=ex - ) + if e.type in self.listeners: + responses = await asyncio.gather( + *[ + listener(e) + for listener in list( + self.listeners[e.type].values() + ) + + list(self.listeners[EventType.ANY].values()) + ] + ) + for res in responses: + if isinstance(res, Exception): + logger.error( + f"Failed to process event {e.type}", + exc_info=res, + ) self.backend.clear_events() def _async_loop(self, new_loop: bool = True) -> None: diff --git a/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py b/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py index da123b103f..4ecbb86af3 100644 --- a/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py +++ b/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py @@ -310,10 +310,10 @@ def _check_studies_state(self) -> None: study_list = self.data_repo_tinydb.get_list_of_studies() - all_done = True + nb_study_done = 0 for study in study_list: - all_done = all_done and (study.finished or study.with_error) + nb_study_done += 1 if (study.finished or study.with_error) else 0 if study.done: try: self.log_tail_manager.stop_tracking( @@ -355,7 +355,8 @@ def _check_studies_state(self) -> None: self.create_update_log(study.name), ) - if all_done: + # we refetch study list here because by the time the import_output is done, maybe some new studies has been added + if nb_study_done == len(self.data_repo_tinydb.get_list_of_studies()): self.stop() @staticmethod @@ -567,8 +568,7 @@ def get_log(self, job_id: str, log_type: LogType) -> Optional[str]: def _create_event_listener(self) -> Callable[[Event], Awaitable[None]]: async def _listen_to_kill_job(event: Event) -> None: - if event.type == EventType.STUDY_JOB_CANCEL_REQUEST: - self.kill_job(event.payload, dispatch=False) + self.kill_job(event.payload, dispatch=False) return _listen_to_kill_job diff --git a/antarest/launcher/model.py b/antarest/launcher/model.py index 9fe16e3395..5ec663243f 100644 --- a/antarest/launcher/model.py +++ b/antarest/launcher/model.py @@ -17,7 +17,7 @@ class LauncherParametersDTO(BaseModel): time_limit: Optional[int] = None xpansion: bool = False xpansion_r_version: bool = False - archive_output: bool = False + archive_output: bool = True output_suffix: Optional[str] = None other_options: Optional[str] = None # add extensions field here diff --git a/antarest/launcher/service.py b/antarest/launcher/service.py index 302420aac9..cd5864f797 100644 --- a/antarest/launcher/service.py +++ b/antarest/launcher/service.py @@ -1,4 +1,3 @@ -import json import logging import os import shutil @@ -24,7 +23,6 @@ from antarest.core.jwt import JWTUser, DEFAULT_ADMIN_USER from antarest.core.model import ( StudyPermissionType, - JSON, ) from antarest.core.requests import ( RequestParameters, @@ -54,7 +52,6 @@ assert_permission, create_permission_from_study, extract_output_name, - fix_study_root, find_single_output_path, ) @@ -404,11 +401,10 @@ def get_log( job_result = self.job_result_repository.get(str(job_id)) if job_result: - # TODO: remove this part of code when study tree zipfile support is implemented launcher_parameters = LauncherParametersDTO.parse_raw( job_result.launcher_params or "{}" ) - if job_result.output_id and not launcher_parameters.archive_output: + if job_result.output_id: if log_type == LogType.STDOUT: launcher_logs = cast( bytes, @@ -500,15 +496,14 @@ def _import_fallback_output( job_output_path = self._get_job_output_fallback_path(job_id) try: + output_name = extract_output_name(output_path, output_suffix_name) os.mkdir(job_output_path) if output_path.suffix != ".zip": imported_output_path = job_output_path / "imported" shutil.copytree(output_path, imported_output_path) - output_name = extract_output_name( - imported_output_path, output_suffix_name - ) imported_output_path.rename(Path(job_output_path, output_name)) else: + shutil.copy( output_path, job_output_path / f"{output_name}.zip" ) @@ -575,7 +570,9 @@ def _import_output( output_true_path.parent / f"{output_true_path.name}.zip" ) - zip_dir(output_true_path, zip_path=zip_path) + zip_dir( + output_true_path, zip_path=zip_path + ) # TODO: remove source dir ? stopwatch.log_elapsed( lambda x: logger.info( f"Zipped output for job {job_id} in {x}s" diff --git a/antarest/main.py b/antarest/main.py index bda9501063..f7eb31ddb3 100644 --- a/antarest/main.py +++ b/antarest/main.py @@ -57,6 +57,8 @@ from antarest.study.storage.rawstudy.watcher import Watcher from antarest.study.web.watcher_blueprint import create_watcher_routes from antarest.tools.admin_lib import clean_locks +from antarest.worker.archive_worker import ArchiveWorker +from antarest.worker.worker import AbstractWorker logger = logging.getLogger(__name__) @@ -65,6 +67,7 @@ class Module(str, Enum): APP = "app" WATCHER = "watcher" MATRIX_GC = "matrix_gc" + ARCHIVE_WORKER = "archive_worker" def parse_arguments() -> argparse.Namespace: @@ -101,11 +104,7 @@ def parse_arguments() -> argparse.Namespace: "--module", dest="module", help="Select a module to run (default is the application server)", - choices=[ - Module.APP.value, - Module.WATCHER.value, - Module.MATRIX_GC.value, - ], + choices=[mod.value for mod in Module], action="store", default=Module.APP.value, required=False, @@ -289,6 +288,14 @@ def create_matrix_gc( ) +def create_worker( + config: Config, event_bus: Optional[IEventBus] = None +) -> AbstractWorker: + if not event_bus: + _, event_bus, _, _, _, _, _ = create_core_services(None, config) + return ArchiveWorker(event_bus, ["archive_test"]) + + def create_services( config: Config, application: Optional[FastAPI], create_all: bool = False ) -> Dict[str, Any]: @@ -551,5 +558,12 @@ def create_env(config_file: Path) -> Dict[str, Any]: init_db(config_file, config, False, None) matrix_gc = create_matrix_gc(config=config, application=None) matrix_gc.start(threaded=False) + elif module == Module.ARCHIVE_WORKER: + res = get_local_path() / "resources" + config = Config.from_yaml_file(res=res, file=config_file) + configure_logger(config) + init_db(config_file, config, False, None) + worker = create_worker(config) + worker.start() else: raise UnknownModuleError(module) diff --git a/antarest/study/business/config_management.py b/antarest/study/business/config_management.py new file mode 100644 index 0000000000..8a5509d1da --- /dev/null +++ b/antarest/study/business/config_management.py @@ -0,0 +1,155 @@ +from enum import Enum +from functools import reduce +from typing import Dict, Any, List, Union + +from antarest.study.business.utils import execute_or_add_commands +from antarest.study.model import ( + RawStudy, + Study, +) +from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.storage.variantstudy.model.command.update_config import ( + UpdateConfig, +) + + +class OutputVariableBase(str, Enum): + OV_COST = "OV. COST" + OP_COST = "OP. COST" + MRG_PRICE = "MRG. PRICE" + CO2_EMIS = "CO2 EMIS." + DTG_BY_PLANT = "DTG by plant" + BALANCE = "BALANCE" + ROW_BAL = "ROW BAL." + PSP = "PSP" + MISC_NDG = "MISC. NDG" + LOAD = "LOAD" + H_ROR = "H. ROR" + WIND = "WIND" + SOLAR = "SOLAR" + NUCLEAR = "NUCLEAR" + LIGNITE = "LIGNITE" + COAL = "COAL" + GAS = "GAS" + OIL = "OIL" + MIX_FUEL = "MIX. FUEL" + MISC_DTG = "MISC. DTG" + H_STOR = "H. STOR" + H_PUMP = "H. PUMP" + H_LEV = "H. LEV" + H_INFL = "H. INFL" + H_OVFL = "H. OVFL" + H_VAL = "H. VAL" + H_COST = "H. COST" + UNSP_ENRG = "UNSP. ENRG" + SPIL_ENRG = "SPIL. ENRG" + LOLD = "LOLD" + LOLP = "LOLP" + AVL_DTG = "AVL DTG" + DTG_MRG = "DTG MRG" + MAX_MRG = "MAX MRG" + NP_COST = "NP COST" + NP_Cost_by_plant = "NP Cost by plant" + NODU = "NODU" + NODU_by_plant = "NODU by plant" + FLOW_LIN = "FLOW LIN." + UCAP_LIN = "UCAP LIN." + LOOP_FLOW = "LOOP FLOW" + FLOW_QUAD = "FLOW QUAD." + CONG_FEE_ALG = "CONG. FEE (ALG.)" + CONG_FEE_ABS = "CONG. FEE (ABS.)" + MARG_COST = "MARG. COST" + CONG_PROD_PLUS = "CONG. PROD +" + CONG_PROD_MINUS = "CONG. PROD -" + HURDLE_COST = "HURDLE COST" + + +class OutputVariable810(str, Enum): + RES_GENERATION_BY_PLANT = "RES generation by plant" + MISC_DTG_2 = "MISC. DTG 2" + MISC_DTG_3 = "MISC. DTG 3" + MISC_DTG_4 = "MISC. DTG 4" + WIND_OFFSHORE = "WIND OFFSHORE" + WIND_ONSHORE = "WIND ONSHORE" + SOLAR_CONCRT = "SOLAR CONCRT." + SOLAR_PV = "SOLAR PV" + SOLAR_ROOFT = "SOLAR ROOFT" + RENW_1 = "RENW. 1" + RENW_2 = "RENW. 2" + RENW_3 = "RENW. 3" + RENW_4 = "RENW. 4" + + +OutputVariable = Union[OutputVariableBase, OutputVariable810] +OUTPUT_VARIABLE_LIST: List[str] = [var.value for var in OutputVariableBase] + [ + var.value for var in OutputVariable810 +] + + +class ConfigManager: + def __init__( + self, + storage_service: StudyStorageService, + ) -> None: + self.storage_service = storage_service + + @staticmethod + def get_output_variables(study: Study) -> List[str]: + version = int(study.version) + if version < 810: + return [var.value for var in OutputVariableBase] + return OUTPUT_VARIABLE_LIST + + def get_thematic_trimming(self, study: Study) -> Dict[str, bool]: + storage_service = self.storage_service.get_storage(study) + file_study = storage_service.get_raw(study) + config = file_study.tree.get(["settings", "generaldata"]) + trimming_config = config.get("variable selection", None) + variable_list = self.get_output_variables(study) + if trimming_config: + if trimming_config.get("selected_vars_reset", True): + return { + var: var not in trimming_config.get("select_var -", []) + for var in variable_list + } + else: + return { + var: var in trimming_config.get("select_var +", []) + for var in variable_list + } + return {var: True for var in variable_list} + + def set_thematic_trimming( + self, study: Study, state: Dict[str, bool] + ) -> None: + file_study = self.storage_service.get_storage(study).get_raw(study) + state_by_active: Dict[bool, List[str]] = reduce( + lambda agg, output: self._agg_states(agg, output, state), + state.keys(), + {True: [], False: []}, + ) + config_data: Dict[str, Any] + if len(state_by_active[True]) > len(state_by_active[False]): + config_data = {"select_var -": state_by_active[False]} + else: + config_data = { + "selected_vars_reset": True, + "select_var +": state_by_active[True], + } + command = UpdateConfig( + target="settings/generaldata/variable selection", + data=config_data, + command_context=self.storage_service.variant_study_service.command_factory.command_context, + ) + execute_or_add_commands( + study, file_study, [command], self.storage_service + ) + + @staticmethod + def _agg_states( + state: Dict[bool, List[str]], + key: str, + ref: Dict[str, bool], + ) -> Dict[bool, List[str]]: + state[ref[key]].append(key) + return state diff --git a/antarest/study/business/link_management.py b/antarest/study/business/link_management.py index 559142fd4c..4556bfd667 100644 --- a/antarest/study/business/link_management.py +++ b/antarest/study/business/link_management.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional, Dict, Any from pydantic import BaseModel @@ -13,21 +13,44 @@ ) +class LinkUIDTO(BaseModel): + color: str + width: float + style: str + + class LinkInfoDTO(BaseModel): area1: str area2: str + ui: Optional[LinkUIDTO] = None class LinkManager: def __init__(self, storage_service: StudyStorageService) -> None: self.storage_service = storage_service - def get_all_links(self, study: Study) -> List[LinkInfoDTO]: + def get_all_links( + self, study: Study, with_ui: bool = False + ) -> List[LinkInfoDTO]: file_study = self.storage_service.get_storage(study).get_raw(study) result = [] for area_id, area in file_study.config.areas.items(): + links_config: Optional[Dict[str, Any]] = None + if with_ui: + links_config = file_study.tree.get( + ["input", "links", area_id, "properties"] + ) for link in area.links: - result.append(LinkInfoDTO(area1=area_id, area2=link)) + ui_info: Optional[LinkUIDTO] = None + if with_ui and links_config and link in links_config: + ui_info = LinkUIDTO( + color=f"{links_config[link].get('colorr', '163')},{links_config[link].get('colorg', '163')},{links_config[link].get('colorb', '163')}", + width=links_config[link].get("link-width", 1), + style=links_config[link].get("link-style", "plain"), + ) + result.append( + LinkInfoDTO(area1=area_id, area2=link, ui=ui_info) + ) return result diff --git a/antarest/study/business/utils.py b/antarest/study/business/utils.py index 7f553b6d91..68c33dd1bb 100644 --- a/antarest/study/business/utils.py +++ b/antarest/study/business/utils.py @@ -6,7 +6,7 @@ from antarest.study.model import Study, RawStudy from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.utils import remove_from_cache +from antarest.study.storage.utils import is_managed from antarest.study.storage.variantstudy.model.command.icommand import ICommand from antarest.study.storage.variantstudy.model.model import CommandDTO @@ -25,6 +25,8 @@ def execute_or_add_commands( raise CommandApplicationError(result.message) executed_commands.append(command) storage_service.variant_study_service.invalidate_cache(study) + if not is_managed(study): + file_study.tree.async_denormalize() else: storage_service.variant_study_service.append_commands( study.id, diff --git a/antarest/study/common/utils.py b/antarest/study/common/utils.py new file mode 100644 index 0000000000..9993ac7d82 --- /dev/null +++ b/antarest/study/common/utils.py @@ -0,0 +1,15 @@ +import tempfile +from pathlib import Path +from typing import Tuple, Any +from zipfile import ZipFile + + +def extract_file_to_tmp_dir( + zip_path: Path, inside_zip_path: Path +) -> Tuple[Path, Any]: + str_inside_zip_path = str(inside_zip_path).replace("\\", "/") + tmp_dir = tempfile.TemporaryDirectory() + with ZipFile(zip_path) as zip_obj: + zip_obj.extract(str_inside_zip_path, tmp_dir.name) + path = Path(tmp_dir.name) / inside_zip_path + return path, tmp_dir diff --git a/antarest/study/main.py b/antarest/study/main.py index 50ca9332bf..1ffb38b843 100644 --- a/antarest/study/main.py +++ b/antarest/study/main.py @@ -23,6 +23,7 @@ from antarest.study.storage.rawstudy.raw_study_service import ( RawStudyService, ) +from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.business.matrix_constants_generator import ( GeneratorMatrixConstants, ) @@ -143,7 +144,7 @@ def build_study_service( ) application.include_router( create_study_variant_routes( - variant_study_service=variant_study_service, + study_service=study_service, config=config, ) ) diff --git a/antarest/study/service.py b/antarest/study/service.py index a114a07ff0..eda37362d3 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -47,12 +47,10 @@ TaskUpdateNotifier, noop_notifier, ) -from antarest.core.utils.utils import concat_files, StopWatch +from antarest.core.utils.utils import StopWatch from antarest.login.model import Group from antarest.login.service import LoginService from antarest.matrixstore.business.matrix_editor import ( - Operation, - MatrixSlice, MatrixEditInstructionDTO, ) from antarest.matrixstore.utils import parse_tsv_matrix @@ -63,8 +61,10 @@ AreaCreationDTO, AreaUI, ) +from antarest.study.business.config_management import ConfigManager from antarest.study.business.link_management import LinkManager, LinkInfoDTO from antarest.study.business.matrix_management import MatrixManager +from antarest.study.business.utils import execute_or_add_commands from antarest.study.business.xpansion_management import ( XpansionManager, XpansionSettingsDTO, @@ -123,6 +123,7 @@ assert_permission, create_permission_from_study, get_start_date, + study_matcher, ) from antarest.study.storage.variantstudy.model.command.icommand import ICommand from antarest.study.storage.variantstudy.model.command.replace_matrix import ( @@ -138,6 +139,7 @@ UpdateRawFile, ) from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy +from antarest.study.storage.variantstudy.model.model import CommandDTO from antarest.study.storage.variantstudy.variant_study_service import ( VariantStudyService, ) @@ -174,6 +176,7 @@ def __init__( self.task_service = task_service self.areas = AreaManager(self.storage_service, self.repository) self.links = LinkManager(self.storage_service) + self.config_manager = ConfigManager(self.storage_service) self.xpansion_manager = XpansionManager(self.storage_service) self.matrix_manager = MatrixManager(self.storage_service) self.cache_service = cache_service @@ -213,7 +216,6 @@ def get( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.storage_service.get_storage(study).get( study, url, depth, formatted ) @@ -234,7 +236,6 @@ def get_comments( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) output: Union[str, JSON] if isinstance(study, RawStudy): output = self.storage_service.get_storage(study).get( @@ -312,12 +313,20 @@ def _get_study_metadatas(self, params: RequestParameters) -> List[Study]: ) def get_studies_information( - self, managed: bool, params: RequestParameters + self, + managed: bool, + name: Optional[str], + workspace: Optional[str], + folder: Optional[str], + params: RequestParameters, ) -> Dict[str, StudyMetadataDTO]: """ Get information for all studies. Args: managed: indicate if just managed studies should be retrieved + name: optional name of the study to match + folder: optional folder prefix of the study to match + workspace: optional workspace of the study to match params: request parameters Returns: List of study information @@ -352,7 +361,8 @@ def get_studies_information( study_dto, StudyPermissionType.READ, raising=False, - ), + ) + and study_matcher(name, workspace, folder), studies.values(), ) } @@ -478,6 +488,17 @@ def update_study_information( ) return new_metadata + def check_study_access( + self, + uuid: str, + permission: StudyPermissionType, + params: RequestParameters, + ) -> Study: + study = self.get_study(uuid) + assert_permission(params.user, study, permission) + self._assert_study_unarchived(study) + return study + def get_study_path(self, uuid: str, params: RequestParameters) -> Path: """ Retrieve study path @@ -1181,7 +1202,6 @@ def get_study_sim_result( """ study = self.get_study(study_id) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) logger.info( "study %s output listing asked by user %s", study_id, @@ -1402,6 +1422,44 @@ def _edit_study_using_command( raise NotImplementedError() return command # for testing purpose + def apply_commands( + self, uuid: str, commands: List[CommandDTO], params: RequestParameters + ) -> None: + study = self.get_study(uuid) + if isinstance(study, VariantStudy): + self.storage_service.variant_study_service.append_commands( + uuid, commands, params + ) + else: + file_study = self.storage_service.raw_study_service.get_raw(study) + assert_permission(params.user, study, StudyPermissionType.WRITE) + self._assert_study_unarchived(study) + parsed_commands: List[ICommand] = [] + for command in commands: + parsed_commands.extend( + self.storage_service.variant_study_service.command_factory.to_icommand( + command + ) + ) + execute_or_add_commands( + study, + file_study, + parsed_commands, + self.storage_service, + ) + self.event_bus.push( + Event( + type=EventType.STUDY_DATA_EDITED, + payload=study.to_json_summary(), + permissions=create_permission_from_study(study), + ) + ) + logger.info( + "Study %s updated by user %s", + uuid, + params.get_user_id(), + ) + def edit_study( self, uuid: str, @@ -1605,7 +1663,6 @@ def get_all_areas( ) -> Union[List[AreaInfoDTO], Dict[str, Any]]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return ( self.areas.get_all_areas_ui_info(study) if ui @@ -1615,12 +1672,12 @@ def get_all_areas( def get_all_links( self, uuid: str, + with_ui: bool, params: RequestParameters, ) -> List[LinkInfoDTO]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) - return self.links.get_all_links(study) + return self.links.get_all_links(study, with_ui) def create_area( self, @@ -1768,7 +1825,9 @@ def archive(self, uuid: str, params: RequestParameters) -> str: def archive_task(notifier: TaskUpdateNotifier) -> TaskResult: study_to_archive = self.get_study(uuid) - self.storage_service.raw_study_service.archive(study_to_archive) + archived_path = self.storage_service.raw_study_service.archive( + study_to_archive + ) study_to_archive.archived = True self.repository.save(study_to_archive) self.event_bus.push( @@ -1825,6 +1884,7 @@ def unarchive_task(notifier: TaskUpdateNotifier) -> TaskResult: study_to_archive, io.BytesIO(fh.read()) ) study_to_archive.archived = False + os.unlink( self.storage_service.raw_study_service.get_archive_path( study_to_archive @@ -1838,6 +1898,7 @@ def unarchive_task(notifier: TaskUpdateNotifier) -> TaskResult: permissions=create_permission_from_study(study), ) ) + remove_from_cache(cache=self.cache_service, root_id=uuid) return TaskResult(success=True, message="ok") return self.task_service.add_task( @@ -1971,7 +2032,6 @@ def get_xpansion_settings( ) -> XpansionSettingsDTO: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_xpansion_settings(study) def update_xpansion_settings( @@ -2005,7 +2065,6 @@ def get_candidate( ) -> XpansionCandidateDTO: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_candidate(study, candidate_name) def get_candidates( @@ -2013,7 +2072,6 @@ def get_candidates( ) -> List[XpansionCandidateDTO]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_candidates(study) def update_xpansion_candidate( @@ -2077,7 +2135,6 @@ def get_single_xpansion_constraints( ) -> bytes: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_single_xpansion_constraints( study, filename ) @@ -2087,7 +2144,6 @@ def get_all_xpansion_constraints( ) -> List[str]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_all_xpansion_constraints(study) def add_capa( @@ -2111,14 +2167,12 @@ def get_single_capa( ) -> JSON: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_single_capa(study, filename) def get_all_capa(self, uuid: str, params: RequestParameters) -> List[str]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - self._assert_study_unarchived(study) return self.xpansion_manager.get_all_capa(study) def update_matrix( diff --git a/antarest/study/storage/abstract_storage_service.py b/antarest/study/storage/abstract_storage_service.py index 0458f6ec22..68f35b428e 100644 --- a/antarest/study/storage/abstract_storage_service.py +++ b/antarest/study/storage/abstract_storage_service.py @@ -274,19 +274,13 @@ def import_output( Path(path_output.parent, output_full_name + extension) ) - if not is_zipped: - data = self.get( - metadata, f"output/{output_full_name}", -1, use_cache=False - ) + data = self.get( + metadata, f"output/{output_full_name}", -1, use_cache=False + ) - if data is None: - self.delete_output(metadata, "imported_output") - raise BadOutputError("The output provided is not conform.") - else: - # TODO: remove this part of code when study tree zipfile support is implemented - logger.warning( - "The imported output is zipped: no check is done" - ) + if data is None: + self.delete_output(metadata, "imported_output") + raise BadOutputError("The output provided is not conform.") except Exception as e: logger.error("Failed to import output", exc_info=e) diff --git a/antarest/study/storage/rawstudy/model/filesystem/bucket_node.py b/antarest/study/storage/rawstudy/model/filesystem/bucket_node.py index 6115bd358d..f3c5413380 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/bucket_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/bucket_node.py @@ -1,7 +1,6 @@ -from typing import Optional, List, Union, Dict, Callable, Any +from typing import Optional, List, Dict, Callable, Any from antarest.core.model import JSON, SUB_JSON -from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.model.filesystem.config.model import ( FileStudyTreeConfig, ) @@ -62,6 +61,7 @@ def save( data: SUB_JSON, url: Optional[List[str]] = None, ) -> None: + self._assert_not_in_zipped_file() if not self.config.path.exists(): self.config.path.mkdir() diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/files.py b/antarest/study/storage/rawstudy/model/filesystem/config/files.py index dc1581841c..6b52c755b2 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/files.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/files.py @@ -1,11 +1,13 @@ import logging import re import tempfile +from enum import Enum from pathlib import Path from typing import List, Dict, Any, Tuple, Optional, cast from zipfile import ZipFile from antarest.core.model import JSON +from antarest.study.common.utils import extract_file_to_tmp_dir from antarest.study.storage.rawstudy.io.reader import ( IniReader, MultipleSameKeysIniReader, @@ -27,6 +29,16 @@ logger = logging.getLogger(__name__) +class FileType(Enum): + TXT = "txt" + SIMPLE_INI = "simple_ini" + MULTI_INI = "multi_ini" + + +class FileTypeNotSupportedException(Exception): + pass + + class ConfigPathBuilder: """ Fetch information need by StudyConfig from filesystem data @@ -50,10 +62,12 @@ def build( study_path ) + study_path_without_zip_extension = study_path.parent / study_path.stem + return FileStudyTreeConfig( study_path=study_path, output_path=output_path or study_path / "output", - path=study_path, + path=study_path_without_zip_extension, study_id=study_id, version=ConfigPathBuilder._parse_version(study_path), areas=ConfigPathBuilder._parse_areas(study_path), @@ -65,19 +79,59 @@ def build( store_new_set=sns, archive_input_series=asi, enr_modelling=enr_modelling, + zip_path=study_path if study_path.suffix == ".zip" else None, ) + @staticmethod + def _extract_data_from_file( + root: Path, + inside_root_path: Path, + file_type: FileType, + multi_ini_keys: Optional[List[str]] = None, + ) -> Any: + tmp_dir = None + if root.suffix == ".zip": + output_data_path, tmp_dir = extract_file_to_tmp_dir( + root, inside_root_path + ) + else: + output_data_path = root / inside_root_path + + try: + if file_type == FileType.TXT: + output_data: Any = output_data_path.read_text().split("\n") + elif file_type == FileType.MULTI_INI: + output_data = MultipleSameKeysIniReader(multi_ini_keys).read( + output_data_path + ) + elif file_type == FileType.SIMPLE_INI: + output_data = IniReader().read(output_data_path) + else: + raise FileTypeNotSupportedException() + finally: + if tmp_dir: + tmp_dir.cleanup() + + return output_data + @staticmethod def _parse_version(path: Path) -> int: - studyinfo = IniReader().read(path / "study.antares") - version: int = studyinfo.get("antares", {}).get("version", -1) + study_info = ConfigPathBuilder._extract_data_from_file( + root=path, + inside_root_path=Path("study.antares"), + file_type=FileType.SIMPLE_INI, + ) + version: int = study_info.get("antares", {}).get("version", -1) return version @staticmethod def _parse_parameters(path: Path) -> Tuple[bool, List[str], str]: - general = MultipleSameKeysIniReader().read( - path / "settings/generaldata.ini" + general = ConfigPathBuilder._extract_data_from_file( + root=path, + inside_root_path=Path("settings/generaldata.ini"), + file_type=FileType.MULTI_INI, ) + store_new_set: bool = general.get("output", {}).get( "storenewset", False ) @@ -90,14 +144,18 @@ def _parse_parameters(path: Path) -> Tuple[bool, List[str], str]: if e.strip() ] enr_modelling: str = general.get("other preferences", {}).get( - "renewables-generation-modelling", "aggregated" + "renewable-generation-modelling", "aggregated" ) return store_new_set, archive_input_series, enr_modelling @staticmethod def _parse_bindings(root: Path) -> List[BindingConstraintDTO]: - bindings = IniReader().read( - root / "input/bindingconstraints/bindingconstraints.ini" + bindings = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path( + "input/bindingconstraints/bindingconstraints.ini" + ), + file_type=FileType.SIMPLE_INI, ) output_list = [] for id, bind in bindings.items(): @@ -124,8 +182,11 @@ def _parse_bindings(root: Path) -> List[BindingConstraintDTO]: @staticmethod def _parse_sets(root: Path) -> Dict[str, DistrictSet]: - json = MultipleSameKeysIniReader(["+", "-"]).read( - root / "input/areas/sets.ini" + json = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path("input/areas/sets.ini"), + file_type=FileType.MULTI_INI, + multi_ini_keys=["+", "-"], ) return { name.lower(): DistrictSet( @@ -144,7 +205,11 @@ def _parse_sets(root: Path) -> Dict[str, DistrictSet]: @staticmethod def _parse_areas(root: Path) -> Dict[str, Area]: - areas = (root / "input/areas/list.txt").read_text().split("\n") + areas = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path("input/areas/list.txt"), + file_type=FileType.TXT, + ) areas = [a for a in areas if a != ""] return { transform_name_to_id(a): ConfigPathBuilder.parse_area(root, a) @@ -171,6 +236,11 @@ def parse_simulation(path: Path) -> Optional["Simulation"]: "^([0-9]{8}-[0-9]{4})(eco|adq)-?(.*)", path.stem ) try: + if path.suffix == ".zip": + zf = ZipFile(path, "r") + error = str("checkIntegrity.txt") not in zf.namelist() + else: + error = not (path / "checkIntegrity.txt").exists() ( nbyears, by_year, @@ -184,7 +254,7 @@ def parse_simulation(path: Path) -> Optional["Simulation"]: nbyears=nbyears, by_year=by_year, synthesis=synthesis, - error=not (path / "checkIntegrity.txt").exists(), + error=error, playlist=playlist, archived=path.suffix == ".zip", ) @@ -259,8 +329,10 @@ def parse_area(root: Path, area: str) -> "Area": @staticmethod def _parse_thermal(root: Path, area: str) -> List[Cluster]: - list_ini = IniReader().read( - root / f"input/thermal/clusters/{area}/list.ini" + list_ini = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path(f"input/thermal/clusters/{area}/list.ini"), + file_type=FileType.SIMPLE_INI, ) return [ Cluster( @@ -273,24 +345,31 @@ def _parse_thermal(root: Path, area: str) -> List[Cluster]: @staticmethod def _parse_renewables(root: Path, area: str) -> List[Cluster]: - ini_path = root / f"input/renewables/clusters/{area}/list.ini" - if not ini_path.exists(): - return [] - - list_ini = IniReader().read(ini_path) - return [ - Cluster( - id=transform_name_to_id(key), - enabled=list_ini.get(key, {}).get("enabled", True), - name=list_ini.get(key, {}).get("name", None), + try: + list_ini = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path( + f"input/renewables/clusters/{area}/list.ini" + ), + file_type=FileType.SIMPLE_INI, ) - for key in list(list_ini.keys()) - ] + return [ + Cluster( + id=transform_name_to_id(key), + enabled=list_ini.get(key, {}).get("enabled", True), + name=list_ini.get(key, {}).get("name", None), + ) + for key in list(list_ini.keys()) + ] + except: + return [] @staticmethod def _parse_links(root: Path, area: str) -> Dict[str, Link]: - properties_ini = IniReader().read( - root / f"input/links/{area}/properties.ini" + properties_ini = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path(f"input/links/{area}/properties.ini"), + file_type=FileType.SIMPLE_INI, ) return { link: Link.from_json(properties_ini[link]) @@ -299,14 +378,20 @@ def _parse_links(root: Path, area: str) -> Dict[str, Link]: @staticmethod def _parse_filters_synthesis(root: Path, area: str) -> List[str]: - filters: str = IniReader().read( - root / f"input/areas/{area}/optimization.ini" - )["filtering"]["filter-synthesis"] + optimization = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path(f"input/areas/{area}/optimization.ini"), + file_type=FileType.SIMPLE_INI, + ) + filters: str = optimization["filtering"]["filter-synthesis"] return Link.split(filters) @staticmethod def _parse_filters_year(root: Path, area: str) -> List[str]: - filters: str = IniReader().read( - root / f"input/areas/{area}/optimization.ini" - )["filtering"]["filter-year-by-year"] + optimization = ConfigPathBuilder._extract_data_from_file( + root=root, + inside_root_path=Path(f"input/areas/{area}/optimization.ini"), + file_type=FileType.SIMPLE_INI, + ) + filters: str = optimization["filtering"]["filter-year-by-year"] return Link.split(filters) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/model.py b/antarest/study/storage/rawstudy/model/filesystem/config/model.py index 8553830d5e..af4f118e3d 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/model.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/model.py @@ -124,6 +124,7 @@ def __init__( archive_input_series: Optional[List[str]] = None, enr_modelling: str = ENR_MODELLING.AGGREGATED.value, cache: Optional[Dict[str, List[str]]] = None, + zip_path: Optional[Path] = None, ): self.study_path = study_path self.path = path @@ -138,8 +139,16 @@ def __init__( self.archive_input_series = archive_input_series or list() self.enr_modelling = enr_modelling self.cache = cache or dict() + self.zip_path = zip_path + + def next_file( + self, name: str, is_output: bool = False + ) -> "FileStudyTreeConfig": + if is_output and name in self.outputs and self.outputs[name].archived: + zip_path: Optional[Path] = self.path / f"{name}.zip" + else: + zip_path = self.zip_path - def next_file(self, name: str) -> "FileStudyTreeConfig": return FileStudyTreeConfig( study_path=self.study_path, output_path=self.output_path, @@ -154,6 +163,7 @@ def next_file(self, name: str) -> "FileStudyTreeConfig": archive_input_series=self.archive_input_series, enr_modelling=self.enr_modelling, cache=self.cache, + zip_path=zip_path, ) def at_file(self, filepath: Path) -> "FileStudyTreeConfig": @@ -276,6 +286,7 @@ class FileStudyTreeConfigDTO(BaseModel): store_new_set: bool = False archive_input_series: List[str] = list() enr_modelling: str = ENR_MODELLING.AGGREGATED.value + zip_path: Optional[Path] = None @staticmethod def from_build_config( @@ -294,6 +305,7 @@ def from_build_config( store_new_set=config.store_new_set, archive_input_series=config.archive_input_series, enr_modelling=config.enr_modelling, + zip_path=config.zip_path, ) def to_build_config(self) -> FileStudyTreeConfig: @@ -310,4 +322,5 @@ def to_build_config(self) -> FileStudyTreeConfig: store_new_set=self.store_new_set, archive_input_series=self.archive_input_series, enr_modelling=self.enr_modelling, + zip_path=self.zip_path, ) diff --git a/antarest/study/storage/rawstudy/model/filesystem/folder_node.py b/antarest/study/storage/rawstudy/model/filesystem/folder_node.py index 373ce7b311..8e60c1f3d9 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/folder_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/folder_node.py @@ -6,7 +6,6 @@ from fastapi import HTTPException from antarest.core.model import JSON, SUB_JSON -from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.model.filesystem.config.model import ( FileStudyTreeConfig, ) @@ -32,7 +31,7 @@ class FolderNode(INode[JSON, SUB_JSON, JSON], ABC): """ Hub node which forward request deeper in tree according to url. Or expand request according to depth. Its children is set node by node following antares tree structure. - Strucuture is implemented in antarest.study.repository.filesystem.root + Structure is implemented in antarest.study.repository.filesystem.root """ def __init__( @@ -143,6 +142,7 @@ def save( (name,), sub_url = self.extract_child(children, url) return children[name].save(data, sub_url) else: + self._assert_not_in_zipped_file() if not self.config.path.exists(): self.config.path.mkdir() assert isinstance(data, Dict) @@ -198,13 +198,13 @@ def extract_child( names = list(children.keys()) if names[0] == "*" else names if names[0] not in children: raise ChildNotFoundError( - f"{names[0]} not a children of {self.__class__.__name__}" + f"{names[0]} not a child of {self.__class__.__name__}" ) child_class = type(children[names[0]]) for name in names: if name not in children: raise ChildNotFoundError( - f"{name} not a children of {self.__class__.__name__}" + f"{name} not a child of {self.__class__.__name__}" ) if type(children[name]) != child_class: raise FilterError("Filter selection has different classes") diff --git a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py index 10411feced..e332ff535d 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py @@ -1,7 +1,6 @@ from typing import List, Optional, cast, Dict, Any, Union from antarest.core.model import JSON, SUB_JSON -from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.io.reader import IniReader from antarest.study.storage.rawstudy.io.reader.ini_reader import IReader from antarest.study.storage.rawstudy.io.writer.ini_writer import ( @@ -15,7 +14,6 @@ ) from antarest.study.storage.rawstudy.model.filesystem.inode import ( INode, - TREE, ) @@ -60,10 +58,21 @@ def _get( if depth == 0: return {} url = url or [] - try: - json = self.reader.read(self.path) - except Exception as e: - raise IniReaderError(self.__class__.__name__, str(e)) + + if self.config.zip_path: + file_path, tmp_dir = self._extract_file_to_tmp_dir() + try: + json = self.reader.read(file_path) + except Exception as e: + raise IniReaderError(self.__class__.__name__, str(e)) + finally: + tmp_dir.cleanup() + else: + try: + json = self.reader.read(self.path) + except Exception as e: + raise IniReaderError(self.__class__.__name__, str(e)) + if len(url) == 2: json = json[url[0]][url[1]] elif len(url) == 1: @@ -92,6 +101,7 @@ def get_node( return output def save(self, data: SUB_JSON, url: Optional[List[str]] = None) -> None: + self._assert_not_in_zipped_file() url = url or [] json = self.reader.read(self.path) if self.path.exists() else {} formatted_data = data diff --git a/antarest/study/storage/rawstudy/model/filesystem/inode.py b/antarest/study/storage/rawstudy/model/filesystem/inode.py index c11512fd3d..f42a7b2e51 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/inode.py +++ b/antarest/study/storage/rawstudy/model/filesystem/inode.py @@ -1,6 +1,12 @@ from abc import ABC, abstractmethod -from typing import List, Optional, Dict, TypeVar, Generic, Any +from pathlib import Path +from typing import List, Optional, Dict, TypeVar, Generic, Any, Tuple +from antarest.core.exceptions import ( + ShouldNotHappenException, + WritingInsideZippedFileException, +) +from antarest.study.common.utils import extract_file_to_tmp_dir from antarest.study.storage.rawstudy.model.filesystem.config.model import ( FileStudyTreeConfig, ) @@ -128,5 +134,35 @@ def _assert_url_end(self, url: Optional[List[str]] = None) -> None: f"url should be fully resolved when arrives on {self.__class__.__name__}" ) + def _extract_file_to_tmp_dir( + self, + ) -> Tuple[Path, Any]: + """ + Happens when the file is inside an archive (aka self.config.zip_file is set) + Unzip the file into a temporary directory. + + Returns: + The actual path of the extracted file + the tmp_dir object which MUST be cleared after use of the file + """ + if self.config.zip_path is None: + raise ShouldNotHappenException() + inside_zip_path = str(self.config.path)[ + len(str(self.config.zip_path)[:-4]) + 1 : + ] + if self.config.zip_path: + return extract_file_to_tmp_dir( + self.config.zip_path, Path(inside_zip_path) + ) + else: + raise ShouldNotHappenException() + + def _assert_not_in_zipped_file(self) -> None: + """Prevents writing inside a zip file""" + if self.config.zip_path: + raise WritingInsideZippedFileException( + "Trying to save inside a zipped file" + ) + TREE = Dict[str, INode[Any, Any, Any]] diff --git a/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py b/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py index b831c88599..cff6ca60bc 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py @@ -1,8 +1,7 @@ from abc import ABC, abstractmethod from pathlib import Path -from typing import Optional, List, Generic, Union, cast +from typing import Optional, List, Generic, Union, cast, Tuple, Any -from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.model.filesystem.config.model import ( FileStudyTreeConfig, ) @@ -30,6 +29,17 @@ def __init__( self.context = context super().__init__(config) + def _get_real_file_path( + self, + ) -> Tuple[Path, Any]: + tmp_dir = None + if self.config.zip_path: + path, tmp_dir = self._extract_file_to_tmp_dir() + + else: + path = self.config.path + return path, tmp_dir + def _get( self, url: Optional[List[str]] = None, @@ -88,6 +98,7 @@ def get_link_path(self) -> Path: def save( self, data: Union[str, bytes, S], url: Optional[List[str]] = None ) -> None: + self._assert_not_in_zipped_file() self._assert_url_end(url) if isinstance(data, str) and self.context.resolver.resolve(data): diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py index a3209b3a74..ead6b0b82c 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py @@ -1,5 +1,6 @@ import logging -from typing import List, Optional +from pathlib import Path +from typing import List, Optional, Any import pandas as pd # type: ignore from pandas.errors import EmptyDataError # type: ignore @@ -12,7 +13,6 @@ from antarest.study.storage.rawstudy.model.filesystem.context import ( ContextServer, ) -from antarest.study.storage.rawstudy.model.filesystem.inode import TREE from antarest.study.storage.rawstudy.model.filesystem.matrix.matrix import ( MatrixNode, ) @@ -37,11 +37,14 @@ def __init__( def parse( self, + file_path: Optional[Path] = None, + tmp_dir: Any = None, ) -> JSON: + file_path = file_path or self.config.path try: stopwatch = StopWatch() matrix: pd.DataFrame = pd.read_csv( - self.config.path, + file_path, sep="\t", dtype=float, header=None, @@ -58,7 +61,7 @@ def parse( return data except EmptyDataError: - logger.warning(f"Empty file found when parsing {self.config.path}") + logger.warning(f"Empty file found when parsing {file_path}") return {} def _dump_json(self, data: JSON) -> None: diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py index d9c509ed51..de170b8d29 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py @@ -1,6 +1,7 @@ import logging from abc import ABC, abstractmethod -from typing import List, Optional, Union +from pathlib import Path +from typing import List, Optional, Union, Any from antarest.core.model import JSON from antarest.study.storage.rawstudy.model.filesystem.config.model import ( @@ -38,7 +39,7 @@ def get_lazy_content( return f"matrixfile://{self.config.path.name}" def normalize(self) -> None: - if self.get_link_path().exists(): + if self.get_link_path().exists() or self.config.zip_path: return matrix = self.parse() @@ -53,6 +54,7 @@ def denormalize(self) -> None: if self.config.path.exists() or not self.get_link_path().exists(): return + logger.info(f"Denormalizing matrix {self.config.path}") uuid = self.get_link_path().read_text() matrix = self.context.resolver.resolve(uuid) if not matrix or not isinstance(matrix, dict): @@ -70,17 +72,24 @@ def load( expanded: bool = False, formatted: bool = True, ) -> Union[bytes, JSON]: + file_path, tmp_dir = self._get_real_file_path() if not formatted: - if self.config.path.exists(): - return self.config.path.read_bytes() + if file_path.exists(): + return file_path.read_bytes() logger.warning(f"Missing file {self.config.path}") + if tmp_dir: + tmp_dir.cleanup() return b"" - return self.parse() + return self.parse(file_path, tmp_dir) @abstractmethod - def parse(self) -> JSON: + def parse( + self, + file_path: Optional[Path] = None, + tmp_dir: Any = None, + ) -> JSON: """ Parse the matrix content """ diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/output_series_matrix.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/output_series_matrix.py index 67dd1b9f44..cdad8fb39e 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/output_series_matrix.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/output_series_matrix.py @@ -1,7 +1,7 @@ import logging -from typing import List, Optional, cast, Union +from pathlib import Path +from typing import List, Optional, cast, Union, Any -import numpy as np import pandas as pd # type: ignore from pandas import DataFrame @@ -56,9 +56,14 @@ def get_lazy_content( ) -> str: return f"matrixfile://{self.config.path.name}" - def parse_dataframe(self) -> DataFrame: + def parse_dataframe( + self, + file_path: Optional[Path] = None, + tmp_dir: Any = None, + ) -> DataFrame: + file_path = file_path or self.config.path df = pd.read_csv( - self.config.path, + file_path, sep="\t", skiprows=4, header=[0, 1, 2], @@ -66,6 +71,9 @@ def parse_dataframe(self) -> DataFrame: float_precision="legacy", ) + if tmp_dir: + tmp_dir.cleanup() + date, body = self.date_serializer.extract_date(df) matrix = rename_unnamed(body).astype(float) @@ -76,8 +84,10 @@ def parse_dataframe(self) -> DataFrame: def parse( self, + file_path: Optional[Path] = None, + tmp_dir: Any = None, ) -> JSON: - matrix = self.parse_dataframe() + matrix = self.parse_dataframe(file_path, tmp_dir) return cast(JSON, matrix.to_dict(orient="split")) def _dump_json(self, data: JSON) -> None: @@ -124,14 +134,19 @@ def load( expanded: bool = False, formatted: bool = True, ) -> Union[bytes, JSON]: + file_path, tmp_dir = self._get_real_file_path() if not formatted: - if self.config.path.exists(): - return self.config.path.read_bytes() + if file_path.exists(): + if tmp_dir: + tmp_dir.cleanup() + return file_path.read_bytes() logger.warning(f"Missing file {self.config.path}") + if tmp_dir: + tmp_dir.cleanup() return b"" - return self.parse() + return self.parse(file_path, tmp_dir) def dump( self, data: Union[bytes, JSON], url: Optional[List[str]] = None diff --git a/antarest/study/storage/rawstudy/model/filesystem/raw_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/raw_file_node.py index 111ff3bc76..fe9c4b90d9 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/raw_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/raw_file_node.py @@ -37,11 +37,19 @@ def load( expanded: bool = False, formatted: bool = True, ) -> bytes: - if self.config.path.exists(): - return self.config.path.read_bytes() - logger.warning(f"Missing file {self.config.path}") - return b"" + file_path, tmp_dir = self._get_real_file_path() + + if file_path.exists(): + bytes = file_path.read_bytes() + else: + logger.warning(f"Missing file {self.config.path}") + bytes = b"" + + if tmp_dir: + tmp_dir.cleanup() + + return bytes def dump(self, data: bytes, url: Optional[List[str]] = None) -> None: self.config.path.parent.mkdir(exist_ok=True, parents=True) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py b/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py index bb0e56e154..39b38c0ea7 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/filestudytree.py @@ -1,3 +1,7 @@ +import logging +from threading import Thread + +from antarest.core.utils.fastapi_sqlalchemy import db from antarest.study.storage.rawstudy.model.filesystem.folder_node import ( FolderNode, ) @@ -26,6 +30,9 @@ ) +logger = logging.getLogger(__name__) + + class FileStudyTree(FolderNode): """ Top level node of antares tree structure @@ -54,3 +61,15 @@ def build(self) -> TREE: children["output"] = Output(self.context, output_config) return children + + def async_denormalize(self) -> Thread: + logger.info( + f"Denormalizing (async) study data for study {self.config.study_id}" + ) + thread = Thread(target=self._threaded_denormalize) + thread.start() + return thread + + def _threaded_denormalize(self) -> None: + with db(): + self.denormalize() diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/areas/list.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/areas/list.py index 9b17b5b826..c6e22df06e 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/areas/list.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/areas/list.py @@ -8,7 +8,6 @@ ) from antarest.study.storage.rawstudy.model.filesystem.inode import ( INode, - TREE, ) @@ -39,10 +38,16 @@ def get( expanded: bool = False, formatted: bool = True, ) -> List[str]: - lines = self.config.path.read_text().split("\n") + if self.config.zip_path: + path, tmp_dir = self._extract_file_to_tmp_dir() + lines = path.read_text().split("\n") + tmp_dir.cleanup() + else: + lines = self.config.path.read_text().split("\n") return [l.strip() for l in lines if l.strip()] def save(self, data: List[str], url: Optional[List[str]] = None) -> None: + self._assert_not_in_zipped_file() self.config.path.write_text("\n".join(data)) def delete(self, url: Optional[List[str]] = None) -> None: diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/output/output.py b/antarest/study/storage/rawstudy/model/filesystem/root/output/output.py index 601e973616..84a25f1a1d 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/output/output.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/output/output.py @@ -1,6 +1,3 @@ -from antarest.study.storage.rawstudy.model.filesystem.config.model import ( - FileStudyTreeConfig, -) from antarest.study.storage.rawstudy.model.filesystem.folder_node import ( FolderNode, ) @@ -14,7 +11,9 @@ class Output(FolderNode): def build(self) -> TREE: children: TREE = { str(s.get_file()): OutputSimulation( - self.context, self.config.next_file(s.get_file()), s + self.context, + self.config.next_file(s.get_file(), is_output=True), + s, ) for i, s in self.config.outputs.items() } diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/ts_numbers/ts_numbers_data.py b/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/ts_numbers/ts_numbers_data.py index caa7a58b79..631c8913c6 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/ts_numbers/ts_numbers_data.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/ts_numbers/ts_numbers_data.py @@ -15,12 +15,20 @@ def load( expanded: bool = False, formatted: bool = True, ) -> List[int]: - if self.config.path.exists(): - with open(self.config.path, "r") as fh: + file_path, tmp_dir = self._get_real_file_path() + + if file_path.exists(): + with open(file_path, "r") as fh: data = fh.readlines() - if len(data) >= 1: - return [int(d) for d in data[1:]] + if tmp_dir: + tmp_dir.cleanup() + + if len(data) >= 1: + return [int(d) for d in data[1:]] + + if tmp_dir: + tmp_dir.cleanup() logger.warning(f"Missing file {self.config.path}") return [] diff --git a/antarest/study/storage/rawstudy/model/helpers.py b/antarest/study/storage/rawstudy/model/helpers.py index 8bb25de6b9..18507bfd19 100644 --- a/antarest/study/storage/rawstudy/model/helpers.py +++ b/antarest/study/storage/rawstudy/model/helpers.py @@ -1,56 +1,26 @@ -import tempfile -from pathlib import Path from typing import Optional, List, cast -from zipfile import ZipFile -from antarest.core.exceptions import ShouldNotHappenException from antarest.core.model import JSON from antarest.core.utils.utils import assert_this -from antarest.study.storage.rawstudy.io.reader import MultipleSameKeysIniReader from antarest.study.storage.rawstudy.model.filesystem.config.files import ( ConfigPathBuilder, ) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import ( - DUPLICATE_KEYS, -) class FileStudyHelpers: @staticmethod def get_config(study: FileStudy, output_id: Optional[str] = None) -> JSON: - config_path = ["settings", "generaldata"] if output_id: - if study.config.outputs[output_id].archived: - # TODO: remove this part of code when study tree zipfile support is implemented - if study.config.output_path is None: - raise ShouldNotHappenException() - output_path = study.config.output_path / f"{output_id}.zip" - tmp_dir = tempfile.TemporaryDirectory() - with ZipFile(output_path, "r") as zip_obj: - zip_obj.extract( - "about-the-study/parameters.ini", - tmp_dir.name, - ) - full_path_parameters = ( - Path(tmp_dir.name) - / "about-the-study" - / "parameters.ini" - ) - config = MultipleSameKeysIniReader(DUPLICATE_KEYS).read( - full_path_parameters - ) - tmp_dir.cleanup() - else: - config_path = [ - "output", - output_id, - "about-the-study", - "parameters", - ] - config = study.tree.get(config_path) + config_path = [ + "output", + output_id, + "about-the-study", + "parameters", + ] + config = study.tree.get(config_path) return config - return study.tree.get(config_path) + return study.tree.get(["settings", "generaldata"]) @staticmethod def save_config(study: FileStudy, config: JSON) -> None: diff --git a/antarest/study/storage/rawstudy/raw_study_service.py b/antarest/study/storage/rawstudy/raw_study_service.py index c0367c08c9..e850aeb002 100644 --- a/antarest/study/storage/rawstudy/raw_study_service.py +++ b/antarest/study/storage/rawstudy/raw_study_service.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import Optional, IO, List from uuid import uuid4 +from zipfile import ZipFile from antarest.core.config import Config from antarest.core.exceptions import ( @@ -119,16 +120,23 @@ def update_from_raw_meta( else: raise e - def exists(self, metadata: RawStudy) -> bool: + def exists(self, study: RawStudy) -> bool: """ Check study exist. Args: - metadata: study + study: study Returns: true if study presents in disk, false else. """ - return (self.get_study_path(metadata) / "study.antares").is_file() + path = self.get_study_path(study) + + if study.archived: + path = self.get_archive_path(study) + zf = ZipFile(path, "r") + return str("study.antares") in zf.namelist() + + return (path / "study.antares").is_file() def get_raw( self, @@ -285,7 +293,7 @@ def import_study(self, metadata: RawStudy, stream: IO[bytes]) -> Study: Returns: new study information. """ - path_study = self.get_study_path(metadata) + path_study = Path(metadata.path) path_study.mkdir() try: @@ -349,10 +357,13 @@ def set_reference_output( self.patch_service.set_reference_output(study, output_id, status) remove_from_cache(self.cache, study.id) - def archive(self, study: RawStudy) -> None: + def archive(self, study: RawStudy) -> Path: archive_path = self.get_archive_path(study) - self.export_study(study, archive_path) + new_study_path = self.export_study(study, archive_path) shutil.rmtree(study.path) + remove_from_cache(cache=self.cache, root_id=study.id) + self.cache.invalidate(study.id) + return new_study_path def get_archive_path(self, study: RawStudy) -> Path: return Path(self.config.storage.archive_dir / f"{study.id}.zip") @@ -366,6 +377,8 @@ def get_study_path(self, metadata: Study) -> Path: Returns: study path """ + if metadata.archived: + return self.get_archive_path(metadata) return Path(metadata.path) def initialize_additional_data(self, raw_study: RawStudy) -> bool: diff --git a/antarest/study/storage/rawstudy/watcher.py b/antarest/study/storage/rawstudy/watcher.py index 3f6539f093..ed4aaa7ad2 100644 --- a/antarest/study/storage/rawstudy/watcher.py +++ b/antarest/study/storage/rawstudy/watcher.py @@ -1,5 +1,6 @@ import logging import re +import tempfile import threading from html import escape from http import HTTPStatus @@ -33,8 +34,8 @@ class Watcher: Files Watcher to listen raw studies changes and trigger a database update. """ - LOCK = Path("watcher") - SCAN_LOCK = Path("scan.lock") + LOCK = Path(tempfile.gettempdir()) / "watcher" + SCAN_LOCK = Path(tempfile.gettempdir()) / "scan.lock" def __init__( self, @@ -128,16 +129,18 @@ def _rec_scan( f"No scan directive file found. Will skip further scan of folder {path}" ) return [] + if (path / "study.antares").exists(): logger.debug(f"Study {path.name} found in {workspace}") return [StudyFolder(path, workspace, groups)] + else: folders: List[StudyFolder] = list() if path.is_dir(): for child in path.iterdir(): try: if ( - child.is_dir() + (child.is_dir()) and any( [ re.search(regex, child.name) @@ -256,7 +259,7 @@ def scan( logger.info( f"Waiting for FileLock to synchronize {directory_path or 'all studies'}" ) - with FileLock(self.config.storage.tmp_dir / Watcher.SCAN_LOCK): + with FileLock(Watcher.SCAN_LOCK): logger.info( f"FileLock acquired to synchronize for {directory_path or 'all studies'}" ) diff --git a/antarest/study/storage/utils.py b/antarest/study/storage/utils.py index 8159ac6bc3..15df17c0c2 100644 --- a/antarest/study/storage/utils.py +++ b/antarest/study/storage/utils.py @@ -7,7 +7,7 @@ from math import ceil from pathlib import Path from time import strptime -from typing import Optional, Union, cast +from typing import Optional, Union, cast, Callable from uuid import uuid4 from zipfile import ZipFile @@ -214,6 +214,23 @@ def create_permission_from_study( ) +def study_matcher( + name: Optional[str], workspace: Optional[str], folder: Optional[str] +) -> Callable[[StudyMetadataDTO], bool]: + def study_match(study: StudyMetadataDTO) -> bool: + if name and not study.name.startswith(name): + return False + if workspace and study.workspace != workspace: + return False + if folder and ( + not study.folder or not study.folder.startswith(folder) + ): + return False + return True + + return study_match + + def assert_permission( user: Optional[JWTUser], study: Optional[Union[Study, StudyMetadataDTO]], diff --git a/antarest/study/storage/variantstudy/business/matrix_constants_generator.py b/antarest/study/storage/variantstudy/business/matrix_constants_generator.py index dd47538dd2..6069acebb8 100644 --- a/antarest/study/storage/variantstudy/business/matrix_constants_generator.py +++ b/antarest/study/storage/variantstudy/business/matrix_constants_generator.py @@ -1,3 +1,5 @@ +import tempfile +from pathlib import Path from typing import Dict from filelock import FileLock @@ -32,7 +34,9 @@ class GeneratorMatrixConstants: def __init__(self, matrix_service: ISimpleMatrixService) -> None: self.hashes: Dict[str, str] = {} self.matrix_service: ISimpleMatrixService = matrix_service - with FileLock("matrix_constant_init.lock"): + with FileLock( + str(Path(tempfile.gettempdir()) / "matrix_constant_init.lock") + ): self._init() def _init(self) -> None: diff --git a/antarest/study/storage/variantstudy/variant_study_service.py b/antarest/study/storage/variantstudy/variant_study_service.py index 04cc8ff2bd..8acf75c9d8 100644 --- a/antarest/study/storage/variantstudy/variant_study_service.py +++ b/antarest/study/storage/variantstudy/variant_study_service.py @@ -221,7 +221,7 @@ def append_commands( study_id: str, commands: List[CommandDTO], params: RequestParameters, - ) -> str: + ) -> None: """ Add command to list of commands (at the end) Args: @@ -245,7 +245,6 @@ def append_commands( ] ) self.invalidate_cache(study) - return str(study.id) def replace_commands( self, diff --git a/antarest/study/web/studies_blueprint.py b/antarest/study/web/studies_blueprint.py index 0750ccc36a..535e4fbd28 100644 --- a/antarest/study/web/studies_blueprint.py +++ b/antarest/study/web/studies_blueprint.py @@ -61,12 +61,15 @@ def create_study_routes( ) def get_studies( managed: bool = False, + name: Optional[str] = None, + folder: Optional[str] = None, + workspace: Optional[str] = None, current_user: JWTUser = Depends(auth.get_current_user), ) -> Any: logger.info(f"Fetching study list", extra={"user": current_user.id}) params = RequestParameters(user=current_user) available_studies = study_service.get_studies_information( - managed, params + managed, name, workspace, folder, params ) return available_studies diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 4aa08e22f3..27246e3a2f 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -5,6 +5,7 @@ from antarest.core.config import Config from antarest.core.jwt import JWTUser +from antarest.core.model import StudyPermissionType from antarest.core.requests import ( RequestParameters, ) @@ -21,6 +22,9 @@ AreaInfoDTO, AreaUI, ) +from antarest.study.business.config_management import ( + OutputVariable, +) from antarest.study.business.link_management import LinkInfoDTO from antarest.study.model import PatchCluster, PatchArea from antarest.study.service import StudyService @@ -71,6 +75,7 @@ def get_areas( ) def get_links( uuid: str, + with_ui: bool = False, current_user: JWTUser = Depends(auth.get_current_user), ) -> Any: logger.info( @@ -78,7 +83,7 @@ def get_links( extra={"user": current_user.id}, ) params = RequestParameters(user=current_user) - areas_list = study_service.get_all_links(uuid, params) + areas_list = study_service.get_all_links(uuid, with_ui, params) return areas_list @bp.post( @@ -220,6 +225,54 @@ def edit_matrix( uuid, path, matrix_edit_instructions, params ) + @bp.get( + "/studies/{uuid}/config/thematic_trimming", + tags=[APITag.study_data], + summary="Get thematic trimming config", + response_model=Dict[str, bool], + ) + def get_thematic_trimming( + uuid: str, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> Any: + logger.info( + f"Fetching thematic trimming config for study {uuid}", + extra={"user": current_user.id}, + ) + params = RequestParameters(user=current_user) + study = study_service.check_study_access( + uuid, StudyPermissionType.READ, params + ) + return study_service.config_manager.get_thematic_trimming(study) + + @bp.put( + "/studies/{uuid}/config/thematic_trimming", + tags=[APITag.study_data], + summary="Set thematic trimming config", + ) + def set_thematic_trimming( + uuid: str, + thematic_trimming_config: Dict[OutputVariable, bool] = Body(...), + current_user: JWTUser = Depends(auth.get_current_user), + ) -> Any: + logger.info( + f"Updating thematic trimming config for study {uuid}", + extra={"user": current_user.id}, + ) + params = RequestParameters(user=current_user) + study = study_service.check_study_access( + uuid, StudyPermissionType.WRITE, params + ) + study_service.config_manager.set_thematic_trimming( + study, + { + output_variable.value: thematic_trimming_config[ + output_variable + ] + for output_variable in thematic_trimming_config + }, + ) + @bp.post( "/studies/_update_version", tags=[APITag.study_data], diff --git a/antarest/study/web/variant_blueprint.py b/antarest/study/web/variant_blueprint.py index dfd3b0775a..cb34fc89a6 100644 --- a/antarest/study/web/variant_blueprint.py +++ b/antarest/study/web/variant_blueprint.py @@ -13,6 +13,8 @@ from antarest.core.utils.web import APITag from antarest.login.auth import Auth from antarest.study.model import StudyMetadataDTO +from antarest.study.service import StudyService +from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.model import ( CommandDTO, VariantTreeDTO, @@ -25,13 +27,13 @@ def create_study_variant_routes( - variant_study_service: VariantStudyService, + study_service: StudyService, config: Config, ) -> APIRouter: """ Endpoint implementation for studies area management Args: - variant_study_service: study service facade to handle request + study_service: study service facade to handle request config: main server configuration Returns: @@ -39,6 +41,7 @@ def create_study_variant_routes( """ bp = APIRouter(prefix="/v1") auth = Auth(config) + variant_study_service = study_service.storage_service.variant_study_service @bp.post( "/studies/{uuid}/variants", @@ -159,16 +162,14 @@ def append_commands( uuid: str, commands: List[CommandDTO] = Body(...), current_user: JWTUser = Depends(auth.get_current_user), - ) -> str: + ) -> None: logger.info( f"Appending new command to variant study {uuid}", extra={"user": current_user.id}, ) params = RequestParameters(user=current_user) sanitized_uuid = sanitize_uuid(uuid) - return variant_study_service.append_commands( - sanitized_uuid, commands, params - ) + study_service.apply_commands(sanitized_uuid, commands, params) @bp.put( "/studies/{uuid}/commands", diff --git a/antarest/worker/__init__.py b/antarest/worker/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/antarest/worker/archive_worker.py b/antarest/worker/archive_worker.py new file mode 100644 index 0000000000..79d715e8e6 --- /dev/null +++ b/antarest/worker/archive_worker.py @@ -0,0 +1,10 @@ +import subprocess +from typing import cast + +from antarest.core.tasks.model import TaskResult +from antarest.worker.worker import AbstractWorker, WorkerTaskCommand + + +class ArchiveWorker(AbstractWorker): + def execute_task(self, task_info: WorkerTaskCommand) -> TaskResult: + raise NotImplementedError diff --git a/antarest/worker/worker.py b/antarest/worker/worker.py new file mode 100644 index 0000000000..7ba5ca5f79 --- /dev/null +++ b/antarest/worker/worker.py @@ -0,0 +1,70 @@ +import abc +import subprocess +import time +from abc import abstractmethod +from concurrent.futures import ThreadPoolExecutor, Future +from threading import Thread +from typing import List, Dict, Union, cast + +from pydantic import BaseModel + +from antarest.core.interfaces.eventbus import IEventBus, Event, EventType +from antarest.core.tasks.model import TaskResult + +MAX_WORKERS = 10 + + +class WorkerTaskResult(BaseModel): + task_id: str + task_result: TaskResult + + +class WorkerTaskCommand(BaseModel): + task_id: str + task_type: str + task_args: Dict[str, Union[int, float, bool, str]] + + +class AbstractWorker(abc.ABC): + def __init__(self, event_bus: IEventBus, accept: List[str]) -> None: + self.event_bus = event_bus + for task_type in accept: + self.event_bus.add_queue_consumer(self.listen_for_tasks, task_type) + self.threadpool = ThreadPoolExecutor( + max_workers=MAX_WORKERS, thread_name_prefix="workertask_" + ) + self.task_watcher = Thread(target=self._loop, daemon=True) + self.futures: Dict[str, Future[TaskResult]] = {} + + def start(self, threaded: bool = False) -> None: + if threaded: + self.task_watcher.start() + else: + self._loop() + + async def listen_for_tasks(self, event: Event) -> None: + task_info = WorkerTaskCommand.parse_obj(event.payload) + self.event_bus.push( + Event(type=EventType.WORKER_TASK_STARTED, payload=task_info) + ) + self.futures[task_info.task_id] = self.threadpool.submit( + self.execute_task, task_info + ) + + @abstractmethod + def execute_task(self, task_info: WorkerTaskCommand) -> TaskResult: + raise NotImplementedError() + + def _loop(self) -> None: + while True: + for task_id, future in self.futures.items(): + if future.done(): + self.event_bus.push( + Event( + type=EventType.WORKER_TASK_ENDED, + payload=WorkerTaskResult( + task_id=task_id, task_result=future.result() + ), + ) + ) + time.sleep(2) diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example new file mode 100644 index 0000000000..478988454a --- /dev/null +++ b/docker-compose.override.yml.example @@ -0,0 +1,10 @@ +version: '2.1' +services: + antares-antarest: + user: UID:GID + antares-antarest-watcher: + user: UID:GID + antares-antarest-matrix-gc: + user: UID:GID + postgresql: + user: UID:GID diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..a18e3bc817 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +version: '2.1' +services: + antares-antarest: + image: antarest:latest + container_name : antarest + environment : + - UVICORN_ROOT_PATH=/api + depends_on: + - redis + - postgresql + volumes: + - ./resources/deploy/examples:/workspaces + - ./resources/deploy/tmp:/antarest_tmp_dir + - ./resources/deploy/matrices:/matrixstore + - ./resources/deploy/config.prod.yaml:/resources/application.yaml + - ./resources/deploy/logs:/logs + - ./resources/deploy/gunicorn.py:/conf/gunicorn.py + - ./antares-8.2.2-Ubuntu-20.04/bin:/antares_simulator + antares-antarest-watcher: + image: antarest:latest + container_name: antarest-watcher + volumes: + - ./resources/deploy/examples:/workspaces + - ./resources/deploy/tmp:/antarest_tmp_dir + - ./resources/deploy/matrices:/matrixstore + - ./resources/deploy/config.prod.yaml:/resources/application.yaml + - ./resources/deploy/logs:/logs + depends_on: + - antares-antarest + command: watcher + antares-antarest-matrix-gc: + image: antarest:latest + container_name : antarest-matrix-gc + volumes: + - ./resources/deploy/examples:/workspaces + - ./resources/deploy/tmp:/antarest_tmp_dir + - ./resources/deploy/matrices:/matrixstore + - ./resources/deploy/config.prod.yaml:/resources/application.yaml + - ./resources/deploy/logs:/logs + depends_on: + - antares-antarest + command: matrix_gc + postgresql: + image: postgres:latest + container_name: postgres + environment: + - POSTGRES_PASSWORD=somepass + - PG_DATA=/var/lib/postgresql/data/pgdata + volumes: + - ./resources/deploy/db:/var/lib/postgresql/data + command: [ "postgres", "-c", "log_statement=all", "-c", "log_destination=stderr" ] + redis: + image: redis:latest + container_name : redis + nginx: + image: nginx:latest + container_name: nginx + depends_on: + - antares-antarest + ports: + - 80:80 + volumes: + - ./resources/deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./webapp/build:/www + - ./resources/deploy/web.config.json:/www/config.json:ro \ No newline at end of file diff --git a/docs/assets/media/img/readme_screenshot.png b/docs/assets/media/img/readme_screenshot.png new file mode 100644 index 0000000000..c555306ecd Binary files /dev/null and b/docs/assets/media/img/readme_screenshot.png differ diff --git a/docs/install/0-INSTALL.md b/docs/install/0-INSTALL.md index e26d939a9a..6703b7e3d8 100644 --- a/docs/install/0-INSTALL.md +++ b/docs/install/0-INSTALL.md @@ -5,7 +5,11 @@ The front end is a [React](https://reactjs.org/) web application. A local build ## Quick start -First clone the projet: +Requirements : +- python : 3.8.x +- node : 14.x + +1. First clone the projet: ``` git clone https://github.com/AntaresSimulatorTeam/AntaREST.git @@ -14,7 +18,7 @@ git submodule init git submodule update ``` -Install back dependencies +2. Install back dependencies ``` python -m pip install --upgrade pip @@ -22,16 +26,15 @@ pip install pydantic --no-binary pydantic pip install -r requirements.txt # use requirements-dev.txt if building a single binary with pyinstaller ``` -Build front +3. Build front (for local mode use `cd ..; ./scripts/build-front.sh` instead of `npm run build`) ``` cd webapp npm install -cd .. -./scripts/build-front.sh +npm run build ``` -Run the application +4. Run the application ``` export PYTHONPATH=$(pwd) @@ -40,3 +43,6 @@ python antarest/main.py -c resources/application.yaml --auto-upgrade-db ## Deploy +There are 2 ways to use and/or deploy the application : +- As [a server application](./2-DEPLOY.md#production-server-deployment) +- As [a desktop systray application](./2-DEPLOY.md#local-application-build) \ No newline at end of file diff --git a/docs/install/2-DEPLOY.md b/docs/install/2-DEPLOY.md index 20deb0f35b..be727fdcd7 100644 --- a/docs/install/2-DEPLOY.md +++ b/docs/install/2-DEPLOY.md @@ -2,15 +2,69 @@ This application can be used in two modes: - a production dockerized environment -- as a local desktop application +- a local desktop application ## Production server deployment -Requires : +The production server deployment uses `docker` and `docker-compose` to run the following containers : +- antarest : the web application workers +- antarest-watcher : the workspace scanner worker +- antarest-matrix-gc: the matrices garbage collector worker +- redis : the cache that allows the multiple application server workers to synchronize +- postgresql : the database +- nginx : the web server front end (can be used to set up ssl) + +The following example shows how to deploy this simple base environment. + +### Example deployment steps + +Requirements : +- a linux host - docker -- redis -- postgresql -- slurm cluster +- docker-compose + +These steps should work on any linux system with docker and docker-compose installed. + +0. First, the steps 1 and 3 of the [quick start build](0-INSTALL.md#quick-start) must have been done. So this guide will assume that you have previously cloned the [code repository](https://github.com/AntaresSimulatorTeam/AntaREST) + (don't forget the git submodule), the frontend built and that your working directory is at the root of the project. + +1. Then download and unzip AntaresSimulator binaries: +``` +wget https://github.com/AntaresSimulatorTeam/Antares_Simulator/releases/download/v8.2.2/antares-8.2.2-Ubuntu-20.04.tar.gz +tar xzf antares-8.2.2-Ubuntu-20.04.tar.gz +``` + +2. Build the docker image +``` +docker build -t antarest:latest . +``` + +3. Prepare the environment (This is important, in order to prevent docker containers to write files into your file system with root permissions.) + a. Copy `docker-compose.override.yml.example` to `docker-compose.override.yml` and replace the UID and GUI values with your user's one. +You can get these values by running the following commands: + - UID: `id -u` + - GID: `id -g` + + b. Create the directory `resources/deploy/db` + + +4. Run the following command to spin up the application containers : +`docker-compose up` + +5. You can then access the application at http://localhost + +6. To stop the application you can juste hit "CTRL-C" to end the containers + +This is an example 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 +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. -## Local application build \ No newline at end of file +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) diff --git a/examples/studies/STA-mini.zip b/examples/studies/STA-mini.zip index b23a787a25..c033602313 100644 Binary files a/examples/studies/STA-mini.zip and b/examples/studies/STA-mini.zip differ diff --git a/resources/deploy/config.prod.yaml b/resources/deploy/config.prod.yaml new file mode 100644 index 0000000000..1bb5e30878 --- /dev/null +++ b/resources/deploy/config.prod.yaml @@ -0,0 +1,80 @@ +security: + disabled: false + jwt: + key: secretkeytochange + login: + admin: + pwd: admin + external_auth: + url: "" + default_group_role: 10 + +db: + url: "postgresql://postgres:somepass@postgresql:5432/postgres" + admin_url: "postgresql://postgres:somepass@postgresql:5432/postgres" + pool_recycle: 3600 + +storage: + tmp_dir: /antarest_tmp_dir + archive_dir: /studies/archives + matrixstore: /matrixstore + matrix_gc_dry_run: true + workspaces: + default: # required, no filters applied, this folder is not watched + path: /workspaces/internal_studies/ + # other workspaces can be added + # if a directory is to be ignored by the watcher, place a file named AW_NO_SCAN inside + tmp: + path: /workspaces/studies/ + # filter_in: ['.*'] # default to '.*' + # filter_out: [] # default to empty + # groups: [] # default empty + +launcher: + default: local + local: + binaries: + 800: /antares_simulator/antares-8.2-solver +# slurm: +# local_workspace: path/to/workspace +# username: username +# hostname: 0.0.0.0 +# port: 22 +# private_key_file: path/to/key +# key_password: key_password +# password: password_is_optional_but_necessary_if_key_is_absent +# default_wait_time: 900 +# default_time_limit: 172800 +# default_n_cpu: 12 +# default_json_db_name: launcher_db.json +# slurm_script_path: /path/to/launchantares_v1.1.3.sh +# db_primary_key: name +# antares_versions_on_remote_server : +# - "610" +# - "700" +# - "710" +# - "720" +# - "800" + + +debug: false + +root_path: "/api" + +#tasks: +# max_workers: 5 +server: + worker_threadpool_size: 12 +# services: +# - watcher + +logging: + level: INFO +# logfile: /logs/antarest.log +# json: true + +# Uncomment these lines to use redis as a backend for the eventbus +# It is required to use redis when using this application on multiple workers in a preforked model like gunicorn for instance +redis: + host: redis + port: 6379 diff --git a/resources/deploy/config.yaml b/resources/deploy/config.yaml index 82c210ce75..e839384ebc 100644 --- a/resources/deploy/config.yaml +++ b/resources/deploy/config.yaml @@ -72,7 +72,6 @@ logging: # Uncomment these lines to use redis as a backend for the eventbus # It is required to use redis when using this application on multiple workers in a preforked model like gunicorn for instance -#eventbus: -# redis: -# host: localhost -# port: 6379 +#redis: +# host: localhost +# port: 6379 diff --git a/resources/deploy/gunicorn.py b/resources/deploy/gunicorn.py new file mode 100644 index 0000000000..26f063db13 --- /dev/null +++ b/resources/deploy/gunicorn.py @@ -0,0 +1,30 @@ +# Reference: https://github.com/benoitc/gunicorn/blob/master/examples/example_config.py + +import multiprocessing +import os + +bind = "0.0.0.0:5000" + +""" +Gunicorn relies on the operating system to provide all of the load balancing +when handling requests. Generally we recommend (2 x $num_cores) + 1 +as the number of workers to start off with. While not overly scientific, +the formula is based on the assumption that for a given core, +one worker will be reading or writing from the socket +while the other worker is processing a request. +https://docs.gunicorn.org/en/stable/design.html#how-many-workers +""" + +workers = os.getenv("GUNICORN_WORKERS") +if workers == "ALL_AVAILABLE" or workers is None: + workers = multiprocessing.cpu_count() * 2 + 1 + +timeout = 10 * 120 # 10 minutes +keepalive = 24 * 60 * 60 # 1 day + +capture_output = True + +loglevel = "info" +errorlog = "/logs/gunicorn.error.log" +accesslog = "/logs/gunicorn.access.log" +preload_app = False diff --git a/resources/deploy/logs/.placeholder b/resources/deploy/logs/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/deploy/nginx.conf b/resources/deploy/nginx.conf new file mode 100644 index 0000000000..393e258292 --- /dev/null +++ b/resources/deploy/nginx.conf @@ -0,0 +1,24 @@ +server { + listen 80; + client_max_body_size 1G; + + # serve static webapp files + location / { + root /www; + try_files $uri $uri/ /index.html = 404; + } + + location /api/ { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_redirect off; + proxy_pass http://antarest:5000/; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 1200; + } +} \ No newline at end of file diff --git a/resources/deploy/web.config.json b/resources/deploy/web.config.json new file mode 100644 index 0000000000..9c89d31d6a --- /dev/null +++ b/resources/deploy/web.config.json @@ -0,0 +1,4 @@ +{ + "restEndpoint": "/api", + "wsEndpoint": "/api/ws" +} \ No newline at end of file diff --git a/setup.py b/setup.py index f708681741..98ea7b806a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="AntaREST", - version="2.5.0", + version="2.5.1", description="Antares Server", long_description=long_description, long_description_content_type="text/markdown", diff --git a/sonar-project.properties b/sonar-project.properties index f651d49bc8..58628f8ce8 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,5 +5,5 @@ sonar.language=python, js sonar.exclusions=antarest/gui.py,antarest/main.py sonar.python.coverage.reportPaths=coverage.xml sonar.javascript.lcov.reportPaths=webapp/coverage/lcov.info -sonar.projectVersion=2.5.0 +sonar.projectVersion=2.5.1 sonar.coverage.exclusions=antarest/gui.py,antarest/main.py,webapp/**/* \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 4bb4e04dec..43689454c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,9 @@ import sys +import time +from datetime import datetime, timedelta from functools import wraps from pathlib import Path -from typing import Any +from typing import Any, Callable from unittest.mock import Mock import pytest @@ -75,3 +77,12 @@ def assert_study(a: SUB_JSON, b: SUB_JSON) -> None: _assert_pointer_path(a, b) else: _assert_others(a, b) + + +def autoretry_assert(func: Callable[..., bool], timeout: int) -> None: + threshold = datetime.utcnow() + timedelta(seconds=timeout) + while datetime.utcnow() < threshold: + if func(): + return + time.sleep(0.2) + raise AssertionError() diff --git a/tests/core/test_tasks.py b/tests/core/test_tasks.py index d99cf45e97..4c1199d707 100644 --- a/tests/core/test_tasks.py +++ b/tests/core/test_tasks.py @@ -1,12 +1,13 @@ import datetime -from typing import Callable +from pathlib import Path +from typing import Callable, List from unittest.mock import Mock, ANY, call import pytest from sqlalchemy import create_engine from antarest.core.config import Config -from antarest.core.interfaces.eventbus import EventType, Event +from antarest.core.interfaces.eventbus import EventType, Event, IEventBus from antarest.core.jwt import DEFAULT_ADMIN_USER from antarest.core.persistence import Base from antarest.core.requests import RequestParameters, UserHasNotPermissionError @@ -22,9 +23,13 @@ from antarest.core.tasks.repository import TaskJobRepository from antarest.core.tasks.service import TaskJobService from antarest.core.utils.fastapi_sqlalchemy import DBSessionMiddleware, db +from antarest.eventbus.business.local_eventbus import LocalEventBus +from antarest.eventbus.service import EventBusService +from antarest.worker.worker import AbstractWorker, WorkerTaskCommand +from tests.conftest import with_db_context -def test_service() -> TaskJobService: +def test_service() -> None: engine = create_engine("sqlite:///:memory:", echo=True) Base.metadata.create_all(engine) DBSessionMiddleware( @@ -256,7 +261,78 @@ def action_ok(update_msg: Callable[[str], None]) -> TaskResult: service.await_task("elsewhere") repo_mock.get.assert_called_with("elsewhere") - return service + +class DummyWorker(AbstractWorker): + def __init__( + self, event_bus: IEventBus, accept: List[str], tmp_path: Path + ): + super().__init__(event_bus, accept) + self.tmp_path = tmp_path + + def execute_task(self, task_info: WorkerTaskCommand) -> TaskResult: + relative_path = task_info.task_args["file"] + (self.tmp_path / relative_path).touch() + return TaskResult(success=True, message="") + + +@with_db_context +def test_worker_tasks(tmp_path: Path): + repo_mock = Mock(spec=TaskJobRepository) + repo_mock.list.return_value = [] + event_bus = EventBusService(LocalEventBus()) + service = TaskJobService( + config=Config(), repository=repo_mock, event_bus=event_bus + ) + + worker = DummyWorker(event_bus, ["test"], tmp_path) + worker.start(threaded=True) + + file_to_create = "foo" + + assert not (tmp_path / file_to_create).exists() + + repo_mock.save.side_effect = [ + TaskJob( + id="taskid", + name="Unnamed", + owner_id=0, + type=TaskType.WORKER_TASK, + ref_id=None, + ), + TaskJob( + id="taskid", + name="Unnamed", + owner_id=0, + type=TaskType.WORKER_TASK, + ref_id=None, + status=TaskStatus.RUNNING, + ), + TaskJob( + id="taskid", + name="Unnamed", + owner_id=0, + type=TaskType.WORKER_TASK, + ref_id=None, + status=TaskStatus.COMPLETED, + ), + ] + repo_mock.get_or_raise.return_value = TaskJob( + id="taskid", + name="Unnamed", + owner_id=0, + type=TaskType.WORKER_TASK, + ref_id=None, + ) + task_id = service.add_worker_task( + "test", + {"file": file_to_create}, + None, + None, + request_params=RequestParameters(user=DEFAULT_ADMIN_USER), + ) + service.await_task(task_id) + + assert (tmp_path / file_to_create).exists() def test_repository(): diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 8843c9ca96..a8a66dd74a 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -3,7 +3,7 @@ import pytest from antarest.core.exceptions import ShouldNotHappenException -from antarest.core.utils.utils import retry, concat_files +from antarest.core.utils.utils import retry, concat_files, suppress_exception def test_retry(): @@ -24,3 +24,12 @@ def test_concat_files(tmp_path: Path): f3.write_text("Done.") concat_files([f1, f2, f3], f_target) assert f_target.read_text(encoding="utf-8") == "hello world !\nDone." + + +def test_suppress_exception(): + def func_failure() -> str: + raise ShouldNotHappenException() + + catched_exc = [] + suppress_exception(func_failure, lambda ex: catched_exc.append(ex)) + assert len(catched_exc) == 1 diff --git a/tests/eventbus/test_local_eventbus.py b/tests/eventbus/test_local_eventbus.py index 7752d89d70..5667a83ba6 100644 --- a/tests/eventbus/test_local_eventbus.py +++ b/tests/eventbus/test_local_eventbus.py @@ -1,10 +1,10 @@ -from antarest.core.interfaces.eventbus import Event +from antarest.core.interfaces.eventbus import Event, EventType from antarest.eventbus.business.local_eventbus import LocalEventBus def test_lifecycle(): eventbus = LocalEventBus() - event = Event(type="test", payload="foo") + event = Event(type=EventType.STUDY_EDITED, payload="foo") eventbus.push_event(event) assert eventbus.get_events() == [event] eventbus.clear_events() diff --git a/tests/eventbus/test_redis_event_bus.py b/tests/eventbus/test_redis_event_bus.py index dba0bd6758..a7a45829a2 100644 --- a/tests/eventbus/test_redis_event_bus.py +++ b/tests/eventbus/test_redis_event_bus.py @@ -2,7 +2,7 @@ import json from unittest.mock import Mock -from antarest.core.interfaces.eventbus import Event +from antarest.core.interfaces.eventbus import Event, EventType from antarest.eventbus.business.redis_eventbus import ( RedisEventBus, ) @@ -15,7 +15,7 @@ def test_lifecycle(): eventbus = RedisEventBus(redis_client) pubsub_mock.subscribe.assert_called_once_with("events") - event = Event(type="test", payload="foo") + event = Event(type=EventType.STUDY_EDITED, payload="foo") serialized = event.json() pubsub_mock.get_message.return_value = {"data": serialized} eventbus.push_event(event) diff --git a/tests/eventbus/test_service.py b/tests/eventbus/test_service.py index bbad16685a..9052b14249 100644 --- a/tests/eventbus/test_service.py +++ b/tests/eventbus/test_service.py @@ -1,20 +1,13 @@ +import asyncio import time from datetime import datetime, timedelta -from typing import Callable +from typing import Callable, List, Awaitable from unittest.mock import Mock, MagicMock from antarest.core.config import Config, EventBusConfig, RedisConfig -from antarest.core.interfaces.eventbus import Event +from antarest.core.interfaces.eventbus import Event, EventType from antarest.eventbus.main import build_eventbus - - -def autoretry(func: Callable[..., bool], timeout: int) -> None: - threshold = datetime.utcnow() + timedelta(seconds=timeout) - while datetime.utcnow() < threshold: - if func(): - return - time.sleep(0.2) - raise AssertionError() +from tests.conftest import autoretry_assert def test_service_factory(): @@ -34,13 +27,36 @@ def test_service_factory(): def test_lifecycle(): event_bus = build_eventbus(MagicMock(), Config(), autostart=True) - test_bucket = [] - lid = event_bus.add_listener(lambda event: test_bucket.append(event)) - event = Event(type="test", payload="foo") - event_bus.push(event) - autoretry(lambda: len(test_bucket) == 1, 2) + test_bucket: List[Event] = [] + + def append_to_bucket( + bucket: List[Event], + ) -> Callable[[Event], Awaitable[None]]: + async def _append_to_bucket(event: Event): + bucket.append(event) - event_bus.remove_listener(lid) + return _append_to_bucket + + lid1 = event_bus.add_listener(append_to_bucket(test_bucket)) + lid2 = event_bus.add_listener( + append_to_bucket(test_bucket), [EventType.STUDY_CREATED] + ) + event_bus.push(Event(type=EventType.STUDY_JOB_STARTED, payload="foo")) + event_bus.push(Event(type=EventType.STUDY_CREATED, payload="foo")) + autoretry_assert(lambda: len(test_bucket) == 3, 2) + + event_bus.remove_listener(lid1) + event_bus.remove_listener(lid2) test_bucket.clear() - event_bus.push(event) - autoretry(lambda: len(test_bucket) == 0, 2) + event_bus.push(Event(type=EventType.STUDY_JOB_STARTED, payload="foo")) + autoretry_assert(lambda: len(test_bucket) == 0, 2) + + queue_name = "some work job" + event_bus.add_queue_consumer(append_to_bucket(test_bucket), queue_name) + event_bus.add_queue_consumer( + lambda event: test_bucket.append(event), queue_name + ) + event_bus.queue( + Event(type=EventType.WORKER_TASK, payload="worker task"), queue_name + ) + autoretry_assert(lambda: len(test_bucket) == 1, 2) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 93b5adbe9d..b61854b243 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -136,7 +136,7 @@ def test_main(app: FastAPI): }, ) res_output = res.json() - assert len(res_output) == 4 + assert len(res_output) == 5 res = client.get( f"/v1/studies/{study_id}/outputs/20201014-1427eco/variables", @@ -174,6 +174,15 @@ def test_main(app: FastAPI): ) assert res.status_code == 200 + # config / thematic trimming + res = client.get( + f"/v1/studies/{study_id}/config/thematic_trimming", + headers={ + "Authorization": f'Bearer {george_credentials["access_token"]}' + }, + ) + assert res.status_code == 200 + # study matrix index res = client.get( f"/v1/studies/{study_id}/matrixindex", @@ -600,12 +609,18 @@ def test_area_management(app: FastAPI): }, ) res_links = client.get( - f"/v1/studies/{study_id}/links", + f"/v1/studies/{study_id}/links?with_ui=true", headers={ "Authorization": f'Bearer {admin_credentials["access_token"]}' }, ) - assert res_links.json() == [{"area1": "area 1", "area2": "area 2"}] + assert res_links.json() == [ + { + "area1": "area 1", + "area2": "area 2", + "ui": {"color": "112,112,112", "style": "plain", "width": 1.0}, + } + ] client.delete( f"/v1/studies/{study_id}/links/area%201/area%202", headers={ diff --git a/tests/launcher/test_service.py b/tests/launcher/test_service.py index 88380a9413..deaa4c1109 100644 --- a/tests/launcher/test_service.py +++ b/tests/launcher/test_service.py @@ -478,7 +478,7 @@ def test_get_logs(tmp_path: Path): JobLog(message="second message", log_type=str(JobLogType.BEFORE)), JobLog(message="last message", log_type=str(JobLogType.AFTER)), ] - job_result_mock.launcher_params = None + job_result_mock.launcher_params = '{"archive_output": false}' launcher_service.job_result_repository.get.return_value = job_result_mock slurm_launcher = Mock() @@ -569,12 +569,18 @@ def test_manage_output(tmp_path: Path): None, JobResult(id=job_id, study_id=study_id), JobResult(id=job_id, study_id=study_id, output_id="some id"), - JobResult(id=job_id, study_id=study_id), + JobResult( + id=job_id, + study_id=study_id, + ), JobResult( id=job_id, study_id=study_id, launcher_params=json.dumps( - {f"{LAUNCHER_PARAM_NAME_SUFFIX}": "hello"} + { + "archive_output": False, + f"{LAUNCHER_PARAM_NAME_SUFFIX}": "hello", + } ), ), ] diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index fa33b6e02a..6d05bfc525 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -410,18 +410,9 @@ def test_get_all_area(): links = link_manager.get_all_links(study) assert [ - { - "area1": "a1", - "area2": "a2", - }, - { - "area1": "a1", - "area2": "a3", - }, - { - "area1": "a2", - "area2": "a3", - }, + {"area1": "a1", "area2": "a2", "ui": None}, + {"area1": "a1", "area2": "a3", "ui": None}, + {"area1": "a2", "area2": "a3", "ui": None}, ] == [link.dict() for link in links] pass diff --git a/tests/storage/business/test_config_manager.py b/tests/storage/business/test_config_manager.py new file mode 100644 index 0000000000..88e0833bd1 --- /dev/null +++ b/tests/storage/business/test_config_manager.py @@ -0,0 +1,105 @@ +from pathlib import Path +from unittest.mock import Mock + +from antarest.study.business.config_management import ( + ConfigManager, + OutputVariableBase, + OutputVariable810, + OUTPUT_VARIABLE_LIST, +) +from antarest.study.storage.rawstudy.model.filesystem.config.model import ( + FileStudyTreeConfig, +) +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy +from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import ( + FileStudyTree, +) +from antarest.study.storage.rawstudy.raw_study_service import RawStudyService +from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.storage.variantstudy.model.command.update_config import ( + UpdateConfig, +) +from antarest.study.storage.variantstudy.model.command_context import ( + CommandContext, +) +from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy +from antarest.study.storage.variantstudy.variant_study_service import ( + VariantStudyService, +) + + +def test_thematic_trimming_config(): + command_context = CommandContext.construct() + command_factory_mock = Mock() + command_factory_mock.command_context = command_context + raw_study_service = Mock(spec=RawStudyService) + variant_study_service = Mock( + spec=VariantStudyService, command_factory=command_factory_mock + ) + config_manager = ConfigManager( + storage_service=StudyStorageService( + raw_study_service, variant_study_service + ), + ) + + study = VariantStudy(version="820") + config = FileStudyTreeConfig( + study_path=Path("somepath"), + path=Path("somepath"), + study_id="", + version=-1, + areas={}, + sets={}, + ) + file_tree_mock = Mock(spec=FileStudyTree, context=Mock(), config=config) + variant_study_service.get_raw.return_value = FileStudy( + config=config, tree=file_tree_mock + ) + file_tree_mock.get.side_effect = [ + {}, + {"variable selection": {"select_var -": ["AVL DTG"]}}, + { + "variable selection": { + "selected_vars_reset": False, + "select_var +": ["CONG. FEE (ALG.)"], + } + }, + ] + + expected = {var: True for var in [var for var in OutputVariableBase]} + study.version = "800" + assert config_manager.get_thematic_trimming(study) == expected + + study.version = "820" + expected = {var: True for var in OUTPUT_VARIABLE_LIST} + expected[OutputVariableBase.AVL_DTG] = False + assert config_manager.get_thematic_trimming(study) == expected + expected = {var: False for var in OUTPUT_VARIABLE_LIST} + expected[OutputVariableBase.CONG_FEE_ALG] = True + assert config_manager.get_thematic_trimming(study) == expected + + new_config = {var: True for var in OUTPUT_VARIABLE_LIST} + new_config[OutputVariableBase.COAL] = False + config_manager.set_thematic_trimming(study, new_config) + assert variant_study_service.append_commands.called_with( + UpdateConfig( + target="settings/generaldata/variable selection", + data={"select_var -": [OutputVariableBase.COAL.value]}, + command_context=command_context, + ) + ) + new_config = {var: False for var in OUTPUT_VARIABLE_LIST} + new_config[OutputVariable810.RENW_1] = True + config_manager.set_thematic_trimming(study, new_config) + assert variant_study_service.append_commands.called_with( + UpdateConfig( + target="settings/generaldata/variable selection", + data={ + "selected_vars_reset": False, + "select_var +": [OutputVariable810.RENW_1.value], + }, + command_context=command_context, + ) + ) + + assert len(OUTPUT_VARIABLE_LIST) == 61 diff --git a/tests/storage/conftest.py b/tests/storage/conftest.py index 9aea4b737f..2bfd3e1d9c 100644 --- a/tests/storage/conftest.py +++ b/tests/storage/conftest.py @@ -3,7 +3,7 @@ import sys import uuid from pathlib import Path -from typing import Callable, Optional, List +from typing import Callable, Optional, List, Dict, Union from unittest.mock import Mock import pytest @@ -293,6 +293,16 @@ def notifier(message: str): return notifier + def add_worker_task( + self, + task_type: str, + task_args: Dict[str, Union[int, float, bool, str]], + name: Optional[str], + ref_id: Optional[str], + request_params: RequestParameters, + ) -> str: + raise NotImplementedError() + def add_task( self, action: Task, diff --git a/tests/storage/repository/filesystem/matrix/test_matrix_node.py b/tests/storage/repository/filesystem/matrix/test_matrix_node.py index 4313b83a7d..870de30002 100644 --- a/tests/storage/repository/filesystem/matrix/test_matrix_node.py +++ b/tests/storage/repository/filesystem/matrix/test_matrix_node.py @@ -1,5 +1,6 @@ import json from pathlib import Path +from tempfile import TemporaryDirectory from typing import Optional, List from unittest.mock import Mock @@ -32,6 +33,8 @@ def __init__( def parse( self, + file_path: Optional[Path] = None, + tmp_dir: Optional[TemporaryDirectory] = None, ) -> JSON: return MOCK_MATRIX_JSON diff --git a/tests/storage/repository/filesystem/test_folder_node.py b/tests/storage/repository/filesystem/test_folder_node.py index 866d16aced..92e927ad84 100644 --- a/tests/storage/repository/filesystem/test_folder_node.py +++ b/tests/storage/repository/filesystem/test_folder_node.py @@ -25,6 +25,7 @@ def build_tree() -> INode: config = Mock() config.path.exist.return_value = True + config.zip_path = None return TestMiddleNode( context=Mock(), config=config, diff --git a/tests/storage/test_model.py b/tests/storage/test_model.py index 923352c7ad..ced31303b4 100644 --- a/tests/storage/test_model.py +++ b/tests/storage/test_model.py @@ -46,7 +46,7 @@ def test_file_study_tree_config_dto(): enr_modelling="aggregated", ) config_dto = FileStudyTreeConfigDTO.from_build_config(config) - assert list(config_dto.dict().keys()) + ["cache"] == list( - config.__dict__.keys() + assert sorted(list(config_dto.dict().keys()) + ["cache"]) == sorted( + list(config.__dict__.keys()) ) assert config_dto.to_build_config() == config diff --git a/tests/storage/test_service.py b/tests/storage/test_service.py index c867215d38..b06d0f3824 100644 --- a/tests/storage/test_service.py +++ b/tests/storage/test_service.py @@ -67,7 +67,7 @@ RawFileNode, ) from antarest.study.storage.rawstudy.raw_study_service import RawStudyService -from antarest.study.storage.utils import assert_permission +from antarest.study.storage.utils import assert_permission, study_matcher from antarest.study.storage.variantstudy.business.matrix_constants_generator import ( GeneratorMatrixConstants, ) @@ -218,6 +218,9 @@ def test_study_listing() -> None: studies = service.get_studies_information( managed=False, + name=None, + workspace=None, + folder=None, params=RequestParameters( user=JWTUser(id=2, impersonator=2, type="users") ), @@ -231,6 +234,9 @@ def test_study_listing() -> None: studies = service.get_studies_information( managed=False, + name=None, + workspace=None, + folder=None, params=RequestParameters( user=JWTUser(id=2, impersonator=2, type="users") ), @@ -242,6 +248,9 @@ def test_study_listing() -> None: cache.get.return_value = None studies = service.get_studies_information( managed=True, + name=None, + workspace=None, + folder=None, params=RequestParameters( user=JWTUser(id=2, impersonator=2, type="users") ), @@ -934,6 +943,32 @@ def test_check_errors(): repo.get.assert_called_once_with("hello world") +@pytest.mark.unit_test +def test_study_match() -> None: + assert not study_matcher(name=None, folder="ab", workspace="hell")( + StudyMetadataDTO.construct(id="1", folder="abc/de", workspace="hello") + ) + assert study_matcher(name=None, folder="ab", workspace="hello")( + StudyMetadataDTO.construct(id="1", folder="abc/de", workspace="hello") + ) + assert not study_matcher(name=None, folder="abd", workspace="hello")( + StudyMetadataDTO.construct(id="1", folder="abc/de", workspace="hello") + ) + assert not study_matcher(name=None, folder="ab", workspace="hello")( + StudyMetadataDTO.construct(id="1", workspace="hello") + ) + assert study_matcher(name="f", folder=None, workspace="hello")( + StudyMetadataDTO.construct( + id="1", name="foo", folder="abc/de", workspace="hello" + ) + ) + assert not study_matcher(name="foob", folder=None, workspace="hell")( + StudyMetadataDTO.construct( + id="1", name="foo", folder="abc/de", workspace="hello" + ) + ) + + @pytest.mark.unit_test def test_assert_permission() -> None: uuid = str(uuid4()) diff --git a/tests/worker/__init__.py b/tests/worker/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py new file mode 100644 index 0000000000..dba50f8368 --- /dev/null +++ b/tests/worker/test_worker.py @@ -0,0 +1,46 @@ +from pathlib import Path +from typing import List +from unittest.mock import MagicMock + +from antarest.core.config import Config +from antarest.core.interfaces.eventbus import IEventBus, Event, EventType +from antarest.core.tasks.model import TaskResult +from antarest.eventbus.main import build_eventbus +from antarest.worker.worker import AbstractWorker, WorkerTaskCommand +from tests.conftest import autoretry_assert + + +class DummyWorker(AbstractWorker): + def __init__( + self, event_bus: IEventBus, accept: List[str], tmp_path: Path + ): + super().__init__(event_bus, accept) + self.tmp_path = tmp_path + + def execute_task(self, task_info: WorkerTaskCommand) -> TaskResult: + relative_path = task_info.task_args["file"] + (self.tmp_path / relative_path).touch() + return TaskResult(success=True, message="") + + +def test_simple_task(tmp_path: Path): + task_queue = "do_stuff" + event_bus = build_eventbus(MagicMock(), Config(), autostart=True) + event_bus.queue( + Event( + type=EventType.WORKER_TASK, + payload=WorkerTaskCommand( + task_type="touch stuff", + task_id="some task", + task_args={"file": "foo"}, + ), + ), + task_queue, + ) + + assert not (tmp_path / "foo").exists() + + worker = DummyWorker(event_bus, [task_queue], tmp_path) + worker.start(threaded=True) + + autoretry_assert(lambda: (tmp_path / "foo").exists(), 2) diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json index eb591242b7..890b2dede6 100644 --- a/webapp/.eslintrc.json +++ b/webapp/.eslintrc.json @@ -5,10 +5,11 @@ }, "extends": [ "eslint:recommended", + "airbnb", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:react/jsx-runtime", - "airbnb", + "plugin:react-hooks/recommended", "airbnb/hooks", "plugin:prettier/recommended" ], @@ -33,7 +34,6 @@ "ecmaVersion": 2018, "sourceType": "module" }, - "plugins": ["react", "react-hooks", "@typescript-eslint"], "rules": { "@typescript-eslint/no-shadow": "off", "@typescript-eslint/no-unused-vars": [ @@ -56,7 +56,6 @@ } ], "import/prefer-default-export": "off", - "no-undef": "off", "no-param-reassign": [ "error", { diff --git a/webapp/package.json b/webapp/package.json index db252b2aff..7fa2c81505 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "antares-web", - "version": "2.5.0", + "version": "2.5.1", "private": true, "dependencies": { "@emotion/react": "11.9.0", diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index af38707f26..535295c87d 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -225,7 +225,7 @@ "study.modelization.links.type": "Type", "study.modelization.links.transmissionCapa": "Transmission capacities", "study.modelization.links.transmissionCapa.infinite": "Infinite", - "study.modelization.links.transmissionCapa.ignore": "Ignore", + "study.modelization.links.transmissionCapa.ignore": "Null", "study.modelization.links.transmissionCapa.enabled": "Enabled", "study.modelization.links.type.ac": "AC", "study.modelization.links.type.dc": "DC", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 98064bb2af..1e1189140b 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -225,7 +225,7 @@ "study.modelization.links.type": "Type", "study.modelization.links.transmissionCapa": "Transmission capacities", "study.modelization.links.transmissionCapa.infinite": "Infinite", - "study.modelization.links.transmissionCapa.ignore": "Ignore", + "study.modelization.links.transmissionCapa.ignore": "Null", "study.modelization.links.transmissionCapa.enabled": "Enabled", "study.modelization.links.type.ac": "AC", "study.modelization.links.type.dc": "DC", diff --git a/webapp/src/common/types.ts b/webapp/src/common/types.ts index fddd524ae0..031d6ebafe 100644 --- a/webapp/src/common/types.ts +++ b/webapp/src/common/types.ts @@ -563,11 +563,21 @@ export interface UpdateAreaUi { color_rgb: Array; } -export interface LinkCreationInfo { +export interface LinkUIInfoDTO { + color: string; + style: string; + width: number; +} + +export interface LinkCreationInfoDTO { area1: string; area2: string; } +export interface LinkInfoWithUI extends LinkCreationInfoDTO { + ui: LinkUIInfoDTO; +} + export interface AreaCreationDTO { name: string; type: object; diff --git a/webapp/src/pages/Api.tsx b/webapp/src/components/App/Api.tsx similarity index 91% rename from webapp/src/pages/Api.tsx rename to webapp/src/components/App/Api.tsx index da098eab96..612194c0b1 100644 --- a/webapp/src/pages/Api.tsx +++ b/webapp/src/components/App/Api.tsx @@ -1,7 +1,7 @@ import { Box } from "@mui/material"; import SwaggerUI from "swagger-ui-react"; import "swagger-ui-react/swagger-ui.css"; -import { getConfig } from "../services/config"; +import { getConfig } from "../../services/config"; function Api() { return ( diff --git a/webapp/src/components/data/DataListing.tsx b/webapp/src/components/App/Data/DataListing.tsx similarity index 97% rename from webapp/src/components/data/DataListing.tsx rename to webapp/src/components/App/Data/DataListing.tsx index 6032055855..af6400b38a 100644 --- a/webapp/src/components/data/DataListing.tsx +++ b/webapp/src/components/App/Data/DataListing.tsx @@ -4,7 +4,7 @@ import { Typography, Box, styled } from "@mui/material"; import AutoSizer from "react-virtualized-auto-sizer"; import { FixedSizeList, areEqual, ListChildComponentProps } from "react-window"; import ArrowRightIcon from "@mui/icons-material/ArrowRight"; -import { MatrixDataSetDTO } from "../../common/types"; +import { MatrixDataSetDTO } from "../../../common/types"; const ROW_ITEM_SIZE = 45; const BUTTONS_SIZE = 40; @@ -72,6 +72,8 @@ const Row = memo((props: ListChildComponentProps) => { ); }, areEqual); +Row.displayName = "Row"; + function DataListing(props: PropsType) { const { datasets = [], selectedItem, setSelectedItem } = props; diff --git a/webapp/src/components/data/DataPropsView.tsx b/webapp/src/components/App/Data/DataPropsView.tsx similarity index 92% rename from webapp/src/components/data/DataPropsView.tsx rename to webapp/src/components/App/Data/DataPropsView.tsx index 7fe7eba4d1..490d8379d2 100644 --- a/webapp/src/components/data/DataPropsView.tsx +++ b/webapp/src/components/App/Data/DataPropsView.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { MatrixDataSetDTO, MatrixInfoDTO } from "../../common/types"; -import PropertiesView from "../common/PropertiesView"; +import { MatrixDataSetDTO, MatrixInfoDTO } from "../../../common/types"; +import PropertiesView from "../../common/PropertiesView"; import DataListing from "./DataListing"; import { StyledListingBox } from "./styles"; diff --git a/webapp/src/components/data/DatasetCreationDialog.tsx b/webapp/src/components/App/Data/DatasetCreationDialog.tsx similarity index 96% rename from webapp/src/components/data/DatasetCreationDialog.tsx rename to webapp/src/components/App/Data/DatasetCreationDialog.tsx index 8acf26533a..2a97ef4e22 100644 --- a/webapp/src/components/data/DatasetCreationDialog.tsx +++ b/webapp/src/components/App/Data/DatasetCreationDialog.tsx @@ -12,12 +12,12 @@ import { useSnackbar } from "notistack"; import { useTranslation } from "react-i18next"; import axios, { AxiosError } from "axios"; import HelpIcon from "@mui/icons-material/Help"; -import { getGroups } from "../../services/api/user"; -import { GroupDTO, MatrixDataSetDTO } from "../../common/types"; +import { getGroups } from "../../../services/api/user"; +import { GroupDTO, MatrixDataSetDTO } from "../../../common/types"; import { saveMatrix } from "./utils"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../common/loaders/SimpleLoader"; -import BasicDialog from "../common/dialogs/BasicDialog"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../common/loaders/SimpleLoader"; +import BasicDialog from "../../common/dialogs/BasicDialog"; import { BoxParamHeader, BoxParam, ParamTitle } from "./styles"; interface PropTypes { @@ -38,6 +38,8 @@ const HelperIcon = forwardRef((props, ref) => { return
; }); +HelperIcon.displayName = "HelperIcon"; + function DatasetCreationDialog(props: PropTypes) { const [t] = useTranslation(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); diff --git a/webapp/src/components/data/MatrixDialog.tsx b/webapp/src/components/App/Data/MatrixDialog.tsx similarity index 85% rename from webapp/src/components/data/MatrixDialog.tsx rename to webapp/src/components/App/Data/MatrixDialog.tsx index f824ec05bd..ddd7a59b85 100644 --- a/webapp/src/components/data/MatrixDialog.tsx +++ b/webapp/src/components/App/Data/MatrixDialog.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; -import { MatrixInfoDTO, MatrixType } from "../../common/types"; -import { getMatrix } from "../../services/api/matrix"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import DataViewerDialog from "../common/dialogs/DataViewerDialog"; +import { MatrixInfoDTO, MatrixType } from "../../../common/types"; +import { getMatrix } from "../../../services/api/matrix"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import DataViewerDialog from "../../common/dialogs/DataViewerDialog"; interface PropTypes { matrixInfo: MatrixInfoDTO; diff --git a/webapp/src/pages/Data.tsx b/webapp/src/components/App/Data/index.tsx similarity index 91% rename from webapp/src/pages/Data.tsx rename to webapp/src/components/App/Data/index.tsx index 0d9295ff1f..4471856f04 100644 --- a/webapp/src/pages/Data.tsx +++ b/webapp/src/components/App/Data/index.tsx @@ -7,24 +7,24 @@ import StorageIcon from "@mui/icons-material/Storage"; import { Box, Typography, IconButton, Tooltip } from "@mui/material"; import EditIcon from "@mui/icons-material/Edit"; import DownloadIcon from "@mui/icons-material/Download"; -import DataPropsView from "../components/data/DataPropsView"; +import DataPropsView from "./DataPropsView"; import { deleteDataSet, exportMatrixDataset, getMatrixList, getExportMatrixUrl, -} from "../services/api/matrix"; -import { MatrixInfoDTO, MatrixDataSetDTO } from "../common/types"; -import DatasetCreationDialog from "../components/data/DatasetCreationDialog"; -import ConfirmationDialog from "../components/common/dialogs/ConfirmationDialog"; -import RootPage from "../components/common/page/RootPage"; -import MatrixDialog from "../components/data/MatrixDialog"; -import useEnqueueErrorSnackbar from "../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../components/common/loaders/SimpleLoader"; -import SplitLayoutView from "../components/common/SplitLayoutView"; -import FileTable from "../components/common/FileTable"; -import { getAuthUser } from "../redux/selectors"; -import useAppSelector from "../redux/hooks/useAppSelector"; +} from "../../../services/api/matrix"; +import { MatrixInfoDTO, MatrixDataSetDTO } from "../../../common/types"; +import DatasetCreationDialog from "./DatasetCreationDialog"; +import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +import RootPage from "../../common/page/RootPage"; +import MatrixDialog from "./MatrixDialog"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../common/loaders/SimpleLoader"; +import SplitLayoutView from "../../common/SplitLayoutView"; +import FileTable from "../../common/FileTable"; +import { getAuthUser } from "../../../redux/selectors"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; function Data() { const [t] = useTranslation(); diff --git a/webapp/src/components/data/styles.ts b/webapp/src/components/App/Data/styles.ts similarity index 100% rename from webapp/src/components/data/styles.ts rename to webapp/src/components/App/Data/styles.ts diff --git a/webapp/src/components/data/utils.tsx b/webapp/src/components/App/Data/utils.tsx similarity index 97% rename from webapp/src/components/data/utils.tsx rename to webapp/src/components/App/Data/utils.tsx index a578e6feda..baf52e158b 100644 --- a/webapp/src/components/data/utils.tsx +++ b/webapp/src/components/App/Data/utils.tsx @@ -3,12 +3,12 @@ import { MatrixDataSetDTO, MatrixDataSetUpdateDTO, MatrixInfoDTO, -} from "../../common/types"; +} from "../../../common/types"; import { createMatrixByImportation, updateDataSet, createDataSet, -} from "../../services/api/matrix"; +} from "../../../services/api/matrix"; const updateMatrix = async ( data: MatrixDataSetDTO, diff --git a/webapp/src/components/settings/Groups/Header.tsx b/webapp/src/components/App/Settings/Groups/Header.tsx similarity index 90% rename from webapp/src/components/settings/Groups/Header.tsx rename to webapp/src/components/App/Settings/Groups/Header.tsx index d6f1f7a0fe..511f4f3cea 100644 --- a/webapp/src/components/settings/Groups/Header.tsx +++ b/webapp/src/components/App/Settings/Groups/Header.tsx @@ -3,10 +3,10 @@ import GroupAddIcon from "@mui/icons-material/GroupAdd"; import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import { useState } from "react"; -import { GroupDetailsDTO } from "../../../common/types"; +import { GroupDetailsDTO } from "../../../../common/types"; import CreateGroupDialog from "./dialog/CreateGroupDialog"; -import { isAuthUserAdmin } from "../../../redux/selectors"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { isAuthUserAdmin } from "../../../../redux/selectors"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; /** * Types diff --git a/webapp/src/components/settings/Groups/dialog/CreateGroupDialog.tsx b/webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx similarity index 91% rename from webapp/src/components/settings/Groups/dialog/CreateGroupDialog.tsx rename to webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx index 4855044fc8..1b590492a7 100644 --- a/webapp/src/components/settings/Groups/dialog/CreateGroupDialog.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx @@ -7,10 +7,10 @@ import { GroupDTO, RoleDetailsDTO, UserDTO, -} from "../../../../common/types"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; -import { createGroup, createRole } from "../../../../services/api/user"; -import { SubmitHandlerData } from "../../../common/Form"; +} from "../../../../../common/types"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { createGroup, createRole } from "../../../../../services/api/user"; +import { SubmitHandlerData } from "../../../../common/Form"; import GroupFormDialog, { GroupFormDialogProps } from "./GroupFormDialog"; /** diff --git a/webapp/src/components/settings/Groups/dialog/GroupFormDialog/GroupForm.tsx b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx similarity index 93% rename from webapp/src/components/settings/Groups/dialog/GroupFormDialog/GroupForm.tsx rename to webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx index a9dd0dc75c..df592e5d7e 100644 --- a/webapp/src/components/settings/Groups/dialog/GroupFormDialog/GroupForm.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx @@ -28,13 +28,13 @@ import { RESERVED_USER_NAMES, ROLE_TYPE_KEYS, } from "../../../utils"; -import { RoleType, UserDTO } from "../../../../../common/types"; -import { roleToString, sortByName } from "../../../../../services/utils"; -import usePromise from "../../../../../hooks/usePromise"; -import { getUsers } from "../../../../../services/api/user"; -import { getAuthUser } from "../../../../../redux/selectors"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { FormObj } from "../../../../common/Form"; +import { RoleType, UserDTO } from "../../../../../../common/types"; +import { roleToString, sortByName } from "../../../../../../services/utils"; +import usePromise from "../../../../../../hooks/usePromise"; +import { getUsers } from "../../../../../../services/api/user"; +import { getAuthUser } from "../../../../../../redux/selectors"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { FormObj } from "../../../../../common/Form"; function GroupForm(props: FormObj) { const { diff --git a/webapp/src/components/settings/Groups/dialog/GroupFormDialog/index.tsx b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/index.tsx similarity index 84% rename from webapp/src/components/settings/Groups/dialog/GroupFormDialog/index.tsx rename to webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/index.tsx index f00bb42286..26ea1d5b65 100644 --- a/webapp/src/components/settings/Groups/dialog/GroupFormDialog/index.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/index.tsx @@ -1,7 +1,7 @@ import FormDialog, { FormDialogProps, -} from "../../../../common/dialogs/FormDialog"; -import { RoleType, UserDTO } from "../../../../../common/types"; +} from "../../../../../common/dialogs/FormDialog"; +import { RoleType, UserDTO } from "../../../../../../common/types"; import GroupForm from "./GroupForm"; /** diff --git a/webapp/src/components/settings/Groups/dialog/UpdateGroupDialog.tsx b/webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx similarity index 94% rename from webapp/src/components/settings/Groups/dialog/UpdateGroupDialog.tsx rename to webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx index 5a17619978..1b61989787 100644 --- a/webapp/src/components/settings/Groups/dialog/UpdateGroupDialog.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx @@ -4,17 +4,17 @@ import { useTranslation } from "react-i18next"; import { usePromise as usePromiseWrapper } from "react-use"; import { useSnackbar } from "notistack"; import * as R from "ramda"; -import { GroupDetailsDTO } from "../../../../common/types"; +import { GroupDetailsDTO } from "../../../../../common/types"; import { createRole, deleteUserRole, getRolesForGroup, updateGroup, -} from "../../../../services/api/user"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +} from "../../../../../services/api/user"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; import GroupFormDialog, { GroupFormDialogProps } from "./GroupFormDialog"; import { GroupEdit } from ".."; -import { SubmitHandlerData } from "../../../common/Form"; +import { SubmitHandlerData } from "../../../../common/Form"; type InheritPropsToOmit = | "title" diff --git a/webapp/src/components/settings/Groups/index.tsx b/webapp/src/components/App/Settings/Groups/index.tsx similarity index 93% rename from webapp/src/components/settings/Groups/index.tsx rename to webapp/src/components/App/Settings/Groups/index.tsx index 278b912f4f..d006e3e91e 100644 --- a/webapp/src/components/settings/Groups/index.tsx +++ b/webapp/src/components/App/Settings/Groups/index.tsx @@ -20,17 +20,17 @@ import EditIcon from "@mui/icons-material/Edit"; import * as R from "ramda"; import GroupIcon from "@mui/icons-material/Group"; import { useSnackbar } from "notistack"; -import { GroupDetailsDTO } from "../../../common/types"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; -import { deleteGroup, getGroups } from "../../../services/api/user"; -import { sortByName } from "../../../services/utils"; -import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import { GroupDetailsDTO } from "../../../../common/types"; +import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; +import { deleteGroup, getGroups } from "../../../../services/api/user"; +import { sortByName } from "../../../../services/utils"; +import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; import { RESERVED_GROUP_NAMES } from "../utils"; import Header from "./Header"; import UpdateGroupDialog from "./dialog/UpdateGroupDialog"; -import { getAuthUser } from "../../../redux/selectors"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getAuthUser } from "../../../../redux/selectors"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; /** * Types diff --git a/webapp/src/components/settings/Maintenance/index.tsx b/webapp/src/components/App/Settings/Maintenance/index.tsx similarity index 93% rename from webapp/src/components/settings/Maintenance/index.tsx rename to webapp/src/components/App/Settings/Maintenance/index.tsx index 9baa9983be..c1ecaec92a 100644 --- a/webapp/src/components/settings/Maintenance/index.tsx +++ b/webapp/src/components/App/Settings/Maintenance/index.tsx @@ -13,15 +13,15 @@ import { useState } from "react"; import { Controller, FieldValues, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useUpdateEffect } from "react-use"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; import { getMaintenanceMode, getMessageInfo, updateMaintenanceMode, updateMessageInfo, -} from "../../../services/api/maintenance"; -import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +} from "../../../../services/api/maintenance"; +import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; function Maintenance() { const { t } = useTranslation(); diff --git a/webapp/src/components/settings/Tokens/Header.tsx b/webapp/src/components/App/Settings/Tokens/Header.tsx similarity index 97% rename from webapp/src/components/settings/Tokens/Header.tsx rename to webapp/src/components/App/Settings/Tokens/Header.tsx index 18ba91b372..ae2c473783 100644 --- a/webapp/src/components/settings/Tokens/Header.tsx +++ b/webapp/src/components/App/Settings/Tokens/Header.tsx @@ -3,7 +3,7 @@ import TokenIcon from "@mui/icons-material/Token"; import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import { useState } from "react"; -import { BotDTO } from "../../../common/types"; +import { BotDTO } from "../../../../common/types"; import CreateTokenDialog from "./dialog/CreateTokenDialog"; /** diff --git a/webapp/src/components/settings/Tokens/dialog/CreateTokenDialog.tsx b/webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx similarity index 92% rename from webapp/src/components/settings/Tokens/dialog/CreateTokenDialog.tsx rename to webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx index 4447dd8b7e..9c847756ce 100644 --- a/webapp/src/components/settings/Tokens/dialog/CreateTokenDialog.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx @@ -10,12 +10,12 @@ import { BotDTO, GroupDTO, RoleType, -} from "../../../../common/types"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; -import { createBot } from "../../../../services/api/user"; -import OkDialog from "../../../common/dialogs/OkDialog"; +} from "../../../../../common/types"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { createBot } from "../../../../../services/api/user"; +import OkDialog from "../../../../common/dialogs/OkDialog"; import TokenFormDialog, { TokenFormDialogProps } from "./TokenFormDialog"; -import { SubmitHandlerData } from "../../../common/Form"; +import { SubmitHandlerData } from "../../../../common/Form"; type InheritPropsToOmit = "title" | "titleIcon" | "onSubmit" | "onCancel"; diff --git a/webapp/src/components/settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx similarity index 94% rename from webapp/src/components/settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx rename to webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx index 43a896ba79..91889d997f 100644 --- a/webapp/src/components/settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx @@ -24,13 +24,16 @@ import { v4 as uuidv4 } from "uuid"; import DeleteIcon from "@mui/icons-material/Delete"; import GroupIcon from "@mui/icons-material/Group"; import { TokenFormDialogProps } from "."; -import { GroupDTO, RoleType } from "../../../../../common/types"; -import usePromise from "../../../../../hooks/usePromise"; -import { getGroups } from "../../../../../services/api/user"; -import { roleToString, sortByName } from "../../../../../services/utils"; +import { GroupDTO, RoleType } from "../../../../../../common/types"; +import usePromise from "../../../../../../hooks/usePromise"; +import { getGroups } from "../../../../../../services/api/user"; +import { roleToString, sortByName } from "../../../../../../services/utils"; import { RESERVED_GROUP_NAMES, ROLE_TYPE_KEYS } from "../../../utils"; -import { getAuthUser, isAuthUserAdmin } from "../../../../../redux/selectors"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; +import { + getAuthUser, + isAuthUserAdmin, +} from "../../../../../../redux/selectors"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; interface Props extends UseFormReturn { onlyPermissions?: TokenFormDialogProps["onlyPermissions"]; diff --git a/webapp/src/components/settings/Tokens/dialog/TokenFormDialog/index.tsx b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/index.tsx similarity index 92% rename from webapp/src/components/settings/Tokens/dialog/TokenFormDialog/index.tsx rename to webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/index.tsx index aa6d5b9714..6da35bd3ad 100644 --- a/webapp/src/components/settings/Tokens/dialog/TokenFormDialog/index.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/index.tsx @@ -1,6 +1,6 @@ import FormDialog, { FormDialogProps, -} from "../../../../common/dialogs/FormDialog"; +} from "../../../../../common/dialogs/FormDialog"; import TokenForm from "./TokenForm"; /** diff --git a/webapp/src/components/settings/Tokens/dialog/TokenInfoDialog.tsx b/webapp/src/components/App/Settings/Tokens/dialog/TokenInfoDialog.tsx similarity index 89% rename from webapp/src/components/settings/Tokens/dialog/TokenInfoDialog.tsx rename to webapp/src/components/App/Settings/Tokens/dialog/TokenInfoDialog.tsx index 21dcd6080a..0bbe11b728 100644 --- a/webapp/src/components/settings/Tokens/dialog/TokenInfoDialog.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/TokenInfoDialog.tsx @@ -3,8 +3,8 @@ import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import InfoIcon from "@mui/icons-material/Info"; import { useMemo } from "react"; -import { BotDetailsDTO, RoleType, UserDTO } from "../../../../common/types"; -import OkDialog, { OkDialogProps } from "../../../common/dialogs/OkDialog"; +import { BotDetailsDTO, RoleType, UserDTO } from "../../../../../common/types"; +import OkDialog, { OkDialogProps } from "../../../../common/dialogs/OkDialog"; import TokenForm from "./TokenFormDialog/TokenForm"; /** diff --git a/webapp/src/components/settings/Tokens/index.tsx b/webapp/src/components/App/Settings/Tokens/index.tsx similarity index 92% rename from webapp/src/components/settings/Tokens/index.tsx rename to webapp/src/components/App/Settings/Tokens/index.tsx index 64805f8f91..49b635b572 100644 --- a/webapp/src/components/settings/Tokens/index.tsx +++ b/webapp/src/components/App/Settings/Tokens/index.tsx @@ -20,21 +20,21 @@ import InfoIcon from "@mui/icons-material/Info"; import TokenIcon from "@mui/icons-material/Token"; import * as R from "ramda"; import { useSnackbar } from "notistack"; -import { BotDTO, BotDetailsDTO, UserDTO } from "../../../common/types"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; +import { BotDTO, BotDetailsDTO, UserDTO } from "../../../../common/types"; +import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; import { deleteBot, getBots, getUser, getUsers, -} from "../../../services/api/user"; -import { isUserAdmin, sortByProp } from "../../../services/utils"; -import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +} from "../../../../services/api/user"; +import { isUserAdmin, sortByProp } from "../../../../services/utils"; +import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; import Header from "./Header"; -import { getAuthUser } from "../../../redux/selectors"; +import { getAuthUser } from "../../../../redux/selectors"; import TokenInfoDialog from "./dialog/TokenInfoDialog"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; /** * Types @@ -123,8 +123,7 @@ function Tokens() { const user = await getUser(authUser.id); return bots.map((bot) => ({ ...bot, user })); }, - { errorMessage: t("settings.error.tokensError") }, - [authUser] + { errorMessage: t("settings.error.tokensError"), deps: [authUser] } ); useUpdateEffect(() => { diff --git a/webapp/src/components/settings/Users/Header.tsx b/webapp/src/components/App/Settings/Users/Header.tsx similarity index 96% rename from webapp/src/components/settings/Users/Header.tsx rename to webapp/src/components/App/Settings/Users/Header.tsx index 42421219e9..fcdbac23d2 100644 --- a/webapp/src/components/settings/Users/Header.tsx +++ b/webapp/src/components/App/Settings/Users/Header.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import { useState } from "react"; import CreateUserDialog from "./dialog/CreateUserDialog"; -import { UserDetailsDTO } from "../../../common/types"; +import { UserDetailsDTO } from "../../../../common/types"; /** * Types diff --git a/webapp/src/components/settings/Users/dialog/CreateUserDialog.tsx b/webapp/src/components/App/Settings/Users/dialog/CreateUserDialog.tsx similarity index 91% rename from webapp/src/components/settings/Users/dialog/CreateUserDialog.tsx rename to webapp/src/components/App/Settings/Users/dialog/CreateUserDialog.tsx index 264f9c088e..6f22da6aed 100644 --- a/webapp/src/components/settings/Users/dialog/CreateUserDialog.tsx +++ b/webapp/src/components/App/Settings/Users/dialog/CreateUserDialog.tsx @@ -8,10 +8,10 @@ import { RoleType, UserDetailsDTO, UserDTO, -} from "../../../../common/types"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; -import { createRole, createUser } from "../../../../services/api/user"; -import { SubmitHandlerData } from "../../../common/Form"; +} from "../../../../../common/types"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { createRole, createUser } from "../../../../../services/api/user"; +import { SubmitHandlerData } from "../../../../common/Form"; import UserFormDialog, { UserFormDialogProps } from "./UserFormDialog"; type InheritPropsToOmit = "title" | "titleIcon" | "onSubmit" | "onCancel"; diff --git a/webapp/src/components/settings/Users/dialog/UpdateUserDialog.tsx b/webapp/src/components/App/Settings/Users/dialog/UpdateUserDialog.tsx similarity index 90% rename from webapp/src/components/settings/Users/dialog/UpdateUserDialog.tsx rename to webapp/src/components/App/Settings/Users/dialog/UpdateUserDialog.tsx index 7b3ff0c996..9ffef0096c 100644 --- a/webapp/src/components/settings/Users/dialog/UpdateUserDialog.tsx +++ b/webapp/src/components/App/Settings/Users/dialog/UpdateUserDialog.tsx @@ -3,12 +3,16 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { usePromise as usePromiseWrapper } from "react-use"; import { useSnackbar } from "notistack"; -import { GroupDTO, RoleType, UserDetailsDTO } from "../../../../common/types"; -import { createRole, deleteUserRoles } from "../../../../services/api/user"; +import { + GroupDTO, + RoleType, + UserDetailsDTO, +} from "../../../../../common/types"; +import { createRole, deleteUserRoles } from "../../../../../services/api/user"; import UserFormDialog, { UserFormDialogProps } from "./UserFormDialog"; import { UserEdit } from ".."; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; -import { SubmitHandlerData } from "../../../common/Form"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { SubmitHandlerData } from "../../../../common/Form"; type InheritPropsToOmit = | "title" diff --git a/webapp/src/components/settings/Users/dialog/UserFormDialog/UserForm.tsx b/webapp/src/components/App/Settings/Users/dialog/UserFormDialog/UserForm.tsx similarity index 95% rename from webapp/src/components/settings/Users/dialog/UserFormDialog/UserForm.tsx rename to webapp/src/components/App/Settings/Users/dialog/UserFormDialog/UserForm.tsx index fb9319f507..f11e76b78b 100644 --- a/webapp/src/components/settings/Users/dialog/UserFormDialog/UserForm.tsx +++ b/webapp/src/components/App/Settings/Users/dialog/UserFormDialog/UserForm.tsx @@ -28,12 +28,12 @@ import { RESERVED_USER_NAMES, ROLE_TYPE_KEYS, } from "../../../utils"; -import { GroupDTO, RoleType } from "../../../../../common/types"; -import { roleToString, sortByName } from "../../../../../services/utils"; -import usePromise from "../../../../../hooks/usePromise"; -import { getGroups } from "../../../../../services/api/user"; +import { GroupDTO, RoleType } from "../../../../../../common/types"; +import { roleToString, sortByName } from "../../../../../../services/utils"; +import usePromise from "../../../../../../hooks/usePromise"; +import { getGroups } from "../../../../../../services/api/user"; import { UserFormDialogProps } from "."; -import { FormObj } from "../../../../common/Form"; +import { FormObj } from "../../../../../common/Form"; interface Props extends FormObj { onlyPermissions?: UserFormDialogProps["onlyPermissions"]; diff --git a/webapp/src/components/settings/Users/dialog/UserFormDialog/index.tsx b/webapp/src/components/App/Settings/Users/dialog/UserFormDialog/index.tsx similarity index 89% rename from webapp/src/components/settings/Users/dialog/UserFormDialog/index.tsx rename to webapp/src/components/App/Settings/Users/dialog/UserFormDialog/index.tsx index c4f3f19442..f3d0d915c2 100644 --- a/webapp/src/components/settings/Users/dialog/UserFormDialog/index.tsx +++ b/webapp/src/components/App/Settings/Users/dialog/UserFormDialog/index.tsx @@ -1,8 +1,8 @@ import { DialogContentText } from "@mui/material"; import FormDialog, { FormDialogProps, -} from "../../../../common/dialogs/FormDialog"; -import { GroupDTO, RoleType } from "../../../../../common/types"; +} from "../../../../../common/dialogs/FormDialog"; +import { GroupDTO, RoleType } from "../../../../../../common/types"; import UserForm from "./UserForm"; /** diff --git a/webapp/src/components/settings/Users/index.tsx b/webapp/src/components/App/Settings/Users/index.tsx similarity index 94% rename from webapp/src/components/settings/Users/index.tsx rename to webapp/src/components/App/Settings/Users/index.tsx index f20e864dac..39f667d66e 100644 --- a/webapp/src/components/settings/Users/index.tsx +++ b/webapp/src/components/App/Settings/Users/index.tsx @@ -20,15 +20,15 @@ import { usePromise as usePromiseWrapper, useUpdateEffect } from "react-use"; import { Action } from "redux"; import { useSnackbar } from "notistack"; import * as R from "ramda"; -import { deleteUser, getUsers } from "../../../services/api/user"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +import { deleteUser, getUsers } from "../../../../services/api/user"; +import usePromiseWithSnackbarError from "../../../../hooks/usePromiseWithSnackbarError"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; import Header from "./Header"; import { RESERVED_USER_NAMES } from "../utils"; -import { UserDetailsDTO } from "../../../common/types"; +import { UserDetailsDTO } from "../../../../common/types"; import UpdateUserDialog from "./dialog/UpdateUserDialog"; -import { sortByName } from "../../../services/utils"; +import { sortByName } from "../../../../services/utils"; /** * Types diff --git a/webapp/src/pages/Settings.tsx b/webapp/src/components/App/Settings/index.tsx similarity index 84% rename from webapp/src/pages/Settings.tsx rename to webapp/src/components/App/Settings/index.tsx index a24e5c3940..d460de40e5 100644 --- a/webapp/src/pages/Settings.tsx +++ b/webapp/src/components/App/Settings/index.tsx @@ -3,13 +3,16 @@ import { TabContext, TabList, TabPanel } from "@mui/lab"; import { Box, Tab } from "@mui/material"; import { SyntheticEvent, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import RootPage from "../components/common/page/RootPage"; -import Groups from "../components/settings/Groups"; -import Maintenance from "../components/settings/Maintenance"; -import Tokens from "../components/settings/Tokens"; -import Users from "../components/settings/Users"; -import useAppSelector from "../redux/hooks/useAppSelector"; -import { isAuthUserAdmin, isAuthUserInGroupAdmin } from "../redux/selectors"; +import RootPage from "../../common/page/RootPage"; +import Groups from "./Groups"; +import Maintenance from "./Maintenance"; +import Tokens from "./Tokens"; +import Users from "./Users"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { + isAuthUserAdmin, + isAuthUserInGroupAdmin, +} from "../../../redux/selectors"; /** * Component diff --git a/webapp/src/components/settings/utils.ts b/webapp/src/components/App/Settings/utils.ts similarity index 82% rename from webapp/src/components/settings/utils.ts rename to webapp/src/components/App/Settings/utils.ts index a73a050848..8d0124a990 100644 --- a/webapp/src/components/settings/utils.ts +++ b/webapp/src/components/App/Settings/utils.ts @@ -1,5 +1,5 @@ import * as RA from "ramda-adjunct"; -import { RoleType } from "../../common/types"; +import { RoleType } from "../../../common/types"; export const RESERVED_USER_NAMES = ["admin"]; export const RESERVED_GROUP_NAMES = ["admin"]; diff --git a/webapp/src/components/singlestudy/Commands/Edition/AddCommandDialog.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/AddCommandDialog.tsx similarity index 97% rename from webapp/src/components/singlestudy/Commands/Edition/AddCommandDialog.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/AddCommandDialog.tsx index d54c01c1dd..49db2c6a6f 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/AddCommandDialog.tsx +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/AddCommandDialog.tsx @@ -1,7 +1,7 @@ import { Autocomplete, Box, Button, TextField } from "@mui/material"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import BasicDialog from "../../../common/dialogs/BasicDialog"; +import BasicDialog from "../../../../common/dialogs/BasicDialog"; import { CommandList } from "./utils"; interface PropTypes { diff --git a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandImportButton.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandImportButton.tsx similarity index 100% rename from webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandImportButton.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandImportButton.tsx diff --git a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx similarity index 98% rename from webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx index a287c2e2f5..84bea0fa9b 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/index.tsx @@ -16,8 +16,8 @@ import { } from "@mui/material"; import { CommandItem } from "../../commandTypes"; import CommandImportButton from "../CommandImportButton"; -import { CommandResultDTO } from "../../../../../../common/types"; -import LogModal from "../../../../../common/LogModal"; +import { CommandResultDTO } from "../../../../../../../common/types"; +import LogModal from "../../../../../../common/LogModal"; import { detailsStyle, DraggableAccorderon, diff --git a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts similarity index 99% rename from webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts rename to webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts index 700dbd9778..55b979cdfc 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListItem/style.ts @@ -1,6 +1,6 @@ import { Accordion, Box, styled } from "@mui/material"; import DeleteIcon from "@mui/icons-material/HighlightOff"; -import { PAPER_BACKGROUND_NO_TRANSPARENCY } from "../../../../../../theme"; +import { PAPER_BACKGROUND_NO_TRANSPARENCY } from "../../../../../../../theme"; export const ItemContainer = styled(Box, { shouldForwardProp: (prop) => prop !== "onTopVisible", diff --git a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx similarity index 96% rename from webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx index 887b813e8f..fa6ab6ab1f 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/DraggableCommands/CommandListView.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import { memo, useEffect, useRef } from "react"; import { FixedSizeList, areEqual, ListChildComponentProps } from "react-window"; import { DragDropContext, @@ -9,7 +9,7 @@ import { import { CommandItem } from "../commandTypes"; import CommandListItem from "./CommandListItem"; -const Row = React.memo((props: ListChildComponentProps) => { +const Row = memo((props: ListChildComponentProps) => { const { data, index, style } = props; const { items, @@ -48,6 +48,8 @@ const Row = React.memo((props: ListChildComponentProps) => { ); }, areEqual); +Row.displayName = "Row"; + export type DraggableListProps = { items: CommandItem[]; generationStatus: boolean; diff --git a/webapp/src/components/singlestudy/Commands/Edition/commandTypes.ts b/webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts similarity index 97% rename from webapp/src/components/singlestudy/Commands/Edition/commandTypes.ts rename to webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts index 041a79e008..c933a939ac 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/commandTypes.ts +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts @@ -1,4 +1,4 @@ -import { CommandResultDTO } from "../../../../common/types"; +import { CommandResultDTO } from "../../../../../common/types"; /* eslint-disable camelcase */ export interface CommandItem { diff --git a/webapp/src/components/singlestudy/Commands/Edition/index.tsx b/webapp/src/components/App/Singlestudy/Commands/Edition/index.tsx similarity index 97% rename from webapp/src/components/singlestudy/Commands/Edition/index.tsx rename to webapp/src/components/App/Singlestudy/Commands/Edition/index.tsx index f9b745d97b..294df8f5ac 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/index.tsx +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/index.tsx @@ -30,7 +30,7 @@ import { replaceCommands, applyCommands, getStudyTask, -} from "../../../../services/api/variant"; +} from "../../../../../services/api/variant"; import AddCommandDialog from "./AddCommandDialog"; import { CommandDTO, @@ -39,18 +39,18 @@ import { CommandResultDTO, TaskEventPayload, TaskStatus, -} from "../../../../common/types"; +} from "../../../../../common/types"; import CommandImportButton from "./DraggableCommands/CommandImportButton"; -import { getTask } from "../../../../services/api/tasks"; +import { getTask } from "../../../../../services/api/tasks"; import { Body, EditHeader, Header, headerIconStyle, Root } from "./style"; -import SimpleLoader from "../../../common/loaders/SimpleLoader"; -import NoContent from "../../../common/page/NoContent"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../common/loaders/SimpleLoader"; +import NoContent from "../../../../common/page/NoContent"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; import { addWsMessageListener, sendWsSubscribeMessage, WsChannel, -} from "../../../../services/webSockets"; +} from "../../../../../services/webSockets"; const logError = debug("antares:variantedition:error"); diff --git a/webapp/src/components/singlestudy/Commands/Edition/style.ts b/webapp/src/components/App/Singlestudy/Commands/Edition/style.ts similarity index 100% rename from webapp/src/components/singlestudy/Commands/Edition/style.ts rename to webapp/src/components/App/Singlestudy/Commands/Edition/style.ts diff --git a/webapp/src/components/singlestudy/Commands/Edition/utils.ts b/webapp/src/components/App/Singlestudy/Commands/Edition/utils.ts similarity index 98% rename from webapp/src/components/singlestudy/Commands/Edition/utils.ts rename to webapp/src/components/App/Singlestudy/Commands/Edition/utils.ts index fc2d6140e1..89b415ef62 100644 --- a/webapp/src/components/singlestudy/Commands/Edition/utils.ts +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/utils.ts @@ -3,7 +3,7 @@ import { CommandResultDTO, TaskDTO, TaskStatus, -} from "../../../../common/types"; +} from "../../../../../common/types"; import { CommandEnum, CommandItem, JsonCommandItem } from "./commandTypes"; export const CommandList = [ diff --git a/webapp/src/components/singlestudy/Commands/index.tsx b/webapp/src/components/App/Singlestudy/Commands/index.tsx similarity index 100% rename from webapp/src/components/singlestudy/Commands/index.tsx rename to webapp/src/components/App/Singlestudy/Commands/index.tsx diff --git a/webapp/src/components/singlestudy/Commands/style.ts b/webapp/src/components/App/Singlestudy/Commands/style.ts similarity index 100% rename from webapp/src/components/singlestudy/Commands/style.ts rename to webapp/src/components/App/Singlestudy/Commands/style.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx similarity index 85% rename from webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx index 78dd2d3416..982d5a1737 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/index.tsx @@ -4,12 +4,12 @@ import { useNavigate } from "react-router"; import { Button, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; -import SingleSelect from "../../../../common/SelectSingle"; -import { GenericInfo, VariantTree } from "../../../../../common/types"; -import { createVariant } from "../../../../../services/api/variant"; -import { createListFromTree } from "../../../../../services/utils"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import BasicDialog from "../../../../common/dialogs/BasicDialog"; +import SingleSelect from "../../../../../common/SelectSingle"; +import { GenericInfo, VariantTree } from "../../../../../../common/types"; +import { createVariant } from "../../../../../../services/api/variant"; +import { createListFromTree } from "../../../../../../services/utils"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import BasicDialog from "../../../../../common/dialogs/BasicDialog"; import { InputContainer, Root } from "./style"; interface Props { diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/style.ts b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/style.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/InformationView/CreateVariantModal/style.ts rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantModal/style.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx similarity index 92% rename from webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx index 0262713501..1afbe47129 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/JobStepper.tsx @@ -10,11 +10,11 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useSnackbar } from "notistack"; import { AxiosError } from "axios"; -import { JobStatus, LaunchJob } from "../../../../../common/types"; -import { convertUTCToLocalTime } from "../../../../../services/utils"; -import { killStudy } from "../../../../../services/api/study"; -import LaunchJobLogView from "../../../../tasks/LaunchJobLogView"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import { JobStatus, LaunchJob } from "../../../../../../common/types"; +import { convertUTCToLocalTime } from "../../../../../../services/utils"; +import { killStudy } from "../../../../../../services/api/study"; +import LaunchJobLogView from "../../../../Tasks/LaunchJobLogView"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; import { CancelContainer, JobRoot, @@ -23,7 +23,7 @@ import { StepLabelRoot, StepLabelRow, } from "./style"; -import ConfirmationDialog from "../../../../common/dialogs/ConfirmationDialog"; +import ConfirmationDialog from "../../../../../common/dialogs/ConfirmationDialog"; export const ColorStatus = { running: "warning.main", diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/index.tsx similarity index 82% rename from webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/index.tsx index 02f026ac0e..7c243baa96 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/index.tsx @@ -3,24 +3,25 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; import HistoryIcon from "@mui/icons-material/History"; +import moment from "moment"; import { LaunchJob, LaunchJobDTO, StudyMetadata, WSEvent, WSMessage, -} from "../../../../../common/types"; +} from "../../../../../../common/types"; import { getStudyJobs, mapLaunchJobDTO, -} from "../../../../../services/api/study"; +} from "../../../../../../services/api/study"; import { addWsMessageListener, sendWsSubscribeMessage, WsChannel, -} from "../../../../../services/webSockets"; +} from "../../../../../../services/webSockets"; import JobStepper from "./JobStepper"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; const TitleHeader = styled(Box)(({ theme }) => ({ display: "flex", @@ -85,12 +86,17 @@ function LauncherHistory(props: Props) { try { const data = await getStudyJobs(sid); setStudyJobs( - data.sort((j1, j2) => - (j1.completionDate || j1.creationDate) > - (j2.completionDate || j2.creationDate) - ? -1 - : 1 - ) + data.sort((j1, j2) => { + const defaultCompletionDate = moment(); + const j1CompletionDate = + j1.completionDate || defaultCompletionDate; + const j2CompletionDate = + j2.completionDate || defaultCompletionDate; + if (j1CompletionDate === j2CompletionDate) { + return j1.creationDate > j2.creationDate ? -1 : 1; + } + return j1CompletionDate > j2CompletionDate ? -1 : 1; + }) ); } catch (e) { enqueueErrorSnackbar( diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/style.ts b/webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/style.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/InformationView/LauncherHistory/style.ts rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/LauncherHistory/style.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx similarity index 98% rename from webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx index 0d2ddc25c7..1ee7c64da7 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered"; import { convertDraftJSToXML, convertXMLToDraftJS } from "../utils"; -import BasicDialog from "../../../../../common/dialogs/BasicDialog"; +import BasicDialog from "../../../../../../common/dialogs/BasicDialog"; import { EditorButton, EditorContainer, diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/style.ts b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/style.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/InformationView/Notes/NodeEditorModal/style.ts rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/NodeEditorModal/style.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/index.tsx similarity index 95% rename from webapp/src/components/singlestudy/HomeView/InformationView/Notes/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/index.tsx index 27a76720ee..de1b4c1be8 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/index.tsx @@ -10,12 +10,12 @@ import { editComments, getComments, getStudySynthesis, -} from "../../../../../services/api/study"; +} from "../../../../../../services/api/study"; import { convertXMLToDraftJS } from "./utils"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; import NoteEditorModal from "./NodeEditorModal"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; const Root = styled(Box)(({ theme }) => ({ flex: "0 0 40%", diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/Notes/utils.ts b/webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/utils.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/InformationView/Notes/utils.ts rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/Notes/utils.ts diff --git a/webapp/src/components/singlestudy/HomeView/InformationView/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx similarity index 78% rename from webapp/src/components/singlestudy/HomeView/InformationView/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx index b865f4d8a1..1b9f366980 100644 --- a/webapp/src/components/singlestudy/HomeView/InformationView/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/index.tsx @@ -3,13 +3,16 @@ import { Paper, Button, Box, Divider } from "@mui/material"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; -import { StudyMetadata, VariantTree } from "../../../../common/types"; +import { StudyMetadata, VariantTree } from "../../../../../common/types"; import CreateVariantModal from "./CreateVariantModal"; import LauncherHistory from "./LauncherHistory"; import Notes from "./Notes"; -import LauncherModal from "../../../studies/LauncherDialog"; -import { copyStudy } from "../../../../services/api/study"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import LauncherDialog from "../../../Studies/LauncherDialog"; +import { + copyStudy, + unarchiveStudy as callUnarchiveStudy, +} from "../../../../../services/api/study"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; interface Props { // eslint-disable-next-line react/no-unused-prop-types @@ -37,6 +40,17 @@ function InformationView(props: Props) { } }; + const unarchiveStudy = async (study: StudyMetadata) => { + try { + await callUnarchiveStudy(study.id); + } catch (e) { + enqueueErrorSnackbar( + t("studies.error.unarchive", { studyname: study.name }), + e as AxiosError + ); + } + }; + return ( {t("global.open")} - {study && ( + {study && !study.archived && ( {study && tree && openVariantModal && ( @@ -120,7 +140,7 @@ function InformationView(props: Props) { /> )} {study && openLauncherModal && ( - setOpenLauncherModal(false)} diff --git a/webapp/src/components/singlestudy/HomeView/Split.css b/webapp/src/components/App/Singlestudy/HomeView/Split.css similarity index 100% rename from webapp/src/components/singlestudy/HomeView/Split.css rename to webapp/src/components/App/Singlestudy/HomeView/Split.css diff --git a/webapp/src/components/singlestudy/HomeView/StudyTreeView/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/index.tsx similarity index 98% rename from webapp/src/components/singlestudy/HomeView/StudyTreeView/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/index.tsx index 023b1753d3..af0cd02a51 100644 --- a/webapp/src/components/singlestudy/HomeView/StudyTreeView/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import * as React from "react"; import { Box, styled } from "@mui/material"; -import { StudyMetadata, VariantTree } from "../../../../common/types"; +import { StudyMetadata, VariantTree } from "../../../../../common/types"; import { StudyTree, getTreeNodes } from "./utils"; import { CIRCLE_RADIUS, diff --git a/webapp/src/components/singlestudy/HomeView/StudyTreeView/treeconfig.ts b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/treeconfig.ts similarity index 100% rename from webapp/src/components/singlestudy/HomeView/StudyTreeView/treeconfig.ts rename to webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/treeconfig.ts diff --git a/webapp/src/components/singlestudy/HomeView/StudyTreeView/utils.ts b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/utils.ts similarity index 98% rename from webapp/src/components/singlestudy/HomeView/StudyTreeView/utils.ts rename to webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/utils.ts index 0dfabb3c59..dc1f4f9cf0 100644 --- a/webapp/src/components/singlestudy/HomeView/StudyTreeView/utils.ts +++ b/webapp/src/components/App/Singlestudy/HomeView/StudyTreeView/utils.ts @@ -3,7 +3,7 @@ import { GenericInfo, StudyMetadata, VariantTree, -} from "../../../../common/types"; +} from "../../../../../common/types"; export interface StudyTree { name: string; diff --git a/webapp/src/components/singlestudy/HomeView/index.tsx b/webapp/src/components/App/Singlestudy/HomeView/index.tsx similarity index 96% rename from webapp/src/components/singlestudy/HomeView/index.tsx rename to webapp/src/components/App/Singlestudy/HomeView/index.tsx index 3220e64c20..9e92b33e0e 100644 --- a/webapp/src/components/singlestudy/HomeView/index.tsx +++ b/webapp/src/components/App/Singlestudy/HomeView/index.tsx @@ -2,7 +2,7 @@ import { useNavigate } from "react-router-dom"; import { Box } from "@mui/material"; import Split from "react-split"; -import { StudyMetadata, VariantTree } from "../../../common/types"; +import { StudyMetadata, VariantTree } from "../../../../common/types"; import "./Split.css"; import StudyTreeView from "./StudyTreeView"; import InformationView from "./InformationView"; diff --git a/webapp/src/components/singlestudy/NavHeader.tsx b/webapp/src/components/App/Singlestudy/NavHeader.tsx similarity index 94% rename from webapp/src/components/singlestudy/NavHeader.tsx rename to webapp/src/components/App/Singlestudy/NavHeader.tsx index 97afb0e7aa..0b1f4ed399 100644 --- a/webapp/src/components/singlestudy/NavHeader.tsx +++ b/webapp/src/components/App/Singlestudy/NavHeader.tsx @@ -34,28 +34,28 @@ import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import { useTranslation } from "react-i18next"; import { indigo } from "@mui/material/colors"; -import { StudyMetadata, VariantTree } from "../../common/types"; -import { STUDIES_HEIGHT_HEADER } from "../../theme"; +import { StudyMetadata, VariantTree } from "../../../common/types"; +import { STUDIES_HEIGHT_HEADER } from "../../../theme"; import { archiveStudy as callArchiveStudy, unarchiveStudy as callUnarchiveStudy, -} from "../../services/api/study"; -import { deleteStudy, toggleFavorite } from "../../redux/ducks/studies"; -import LauncherDialog from "../studies/LauncherDialog"; +} from "../../../services/api/study"; +import { deleteStudy, toggleFavorite } from "../../../redux/ducks/studies"; +import LauncherDialog from "../Studies/LauncherDialog"; import PropertiesDialog from "./PropertiesDialog"; import { buildModificationDate, convertUTCToLocalTime, countAllChildrens, displayVersionName, -} from "../../services/utils"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import { isCurrentStudyFavorite } from "../../redux/selectors"; -import ExportDialog from "../studies/ExportModal"; -import StarToggle from "../common/StarToggle"; -import ConfirmationDialog from "../common/dialogs/ConfirmationDialog"; -import useAppSelector from "../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; +} from "../../../services/utils"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import { isCurrentStudyFavorite } from "../../../redux/selectors"; +import ExportDialog from "../Studies/ExportModal"; +import StarToggle from "../../common/StarToggle"; +import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; const logError = debug("antares:singlestudy:navheader:error"); @@ -296,14 +296,16 @@ function NavHeader(props: Props) { alignItems="center" boxSizing="border-box" > - {isExplorer === true && ( + {isExplorer && ( )} {study && study.type === "variantstudy" && ( diff --git a/webapp/src/components/singlestudy/PropertiesDialog/index.tsx b/webapp/src/components/App/Singlestudy/PropertiesDialog/index.tsx similarity index 93% rename from webapp/src/components/singlestudy/PropertiesDialog/index.tsx rename to webapp/src/components/App/Singlestudy/PropertiesDialog/index.tsx index 6e42f7ef18..18d35850d2 100644 --- a/webapp/src/components/singlestudy/PropertiesDialog/index.tsx +++ b/webapp/src/components/App/Singlestudy/PropertiesDialog/index.tsx @@ -5,26 +5,26 @@ import { Button, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; import { useSnackbar } from "notistack"; -import SingleSelect from "../../common/SelectSingle"; -import MultiSelect from "../../common/SelectMulti"; +import SingleSelect from "../../../common/SelectSingle"; +import MultiSelect from "../../../common/SelectMulti"; import { GenericInfo, GroupDTO, StudyMetadata, StudyMetadataPatchDTO, StudyPublicMode, -} from "../../../common/types"; -import TextSeparator from "../../common/TextSeparator"; +} from "../../../../common/types"; +import TextSeparator from "../../../common/TextSeparator"; import { addStudyGroup, changePublicMode, deleteStudyGroup, updateStudyMetadata, -} from "../../../services/api/study"; -import { getGroups } from "../../../services/api/user"; -import TagTextInput from "../../common/TagTextInput"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import BasicDialog from "../../common/dialogs/BasicDialog"; +} from "../../../../services/api/study"; +import { getGroups } from "../../../../services/api/user"; +import TagTextInput from "../../../common/TagTextInput"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import BasicDialog from "../../../common/dialogs/BasicDialog"; import { ElementContainer, InputElement, Root } from "./style"; const logErr = debug("antares:createstudyform:error"); diff --git a/webapp/src/components/singlestudy/PropertiesDialog/style.ts b/webapp/src/components/App/Singlestudy/PropertiesDialog/style.ts similarity index 100% rename from webapp/src/components/singlestudy/PropertiesDialog/style.ts rename to webapp/src/components/App/Singlestudy/PropertiesDialog/style.ts diff --git a/webapp/src/components/singlestudy/explore/Configuration/AdvancedParameters/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/AdvancedParameters/index.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Configuration/AdvancedParameters/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/AdvancedParameters/index.tsx index b30e280a21..e483e81600 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/AdvancedParameters/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/AdvancedParameters/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; function AdvancedParameters() { return ; diff --git a/webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx similarity index 86% rename from webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx index aedd662ce0..4047c38d07 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/General/Fields/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/Fields/index.tsx @@ -3,18 +3,19 @@ import { Box, Divider, TextField } from "@mui/material"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { StyledFieldset } from "../styles"; -import SelectFE from "../../../../../common/fieldEditors/SelectFE"; -import { StudyMetadata } from "../../../../../../common/types"; -import { editStudy } from "../../../../../../services/api/study"; -import SwitchFE from "../../../../../common/fieldEditors/SwitchFE"; +import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; +import { StudyMetadata } from "../../../../../../../common/types"; +import { editStudy } from "../../../../../../../services/api/study"; +import SwitchFE from "../../../../../../common/fieldEditors/SwitchFE"; import { FIRST_JANUARY_OPTIONS, FormValues, WEEK_OPTIONS, YEAR_OPTIONS, } from "../utils"; -import BooleanFE from "../../../../../common/fieldEditors/BooleanFE"; -import { useFormContext } from "../../../../../common/Form"; +import BooleanFE from "../../../../../../common/fieldEditors/BooleanFE"; +import { useFormContext } from "../../../../../../common/Form"; +import useDebouncedEffect from "../../../../../../../hooks/useDebouncedEffect"; interface Props { study: StudyMetadata; @@ -27,7 +28,11 @@ function Fields(props: Props) { const studyVersion = Number(study.version); const [t] = useTranslation(); const { register, setValue, watch, getValues } = useFormContext(); - const buildingMode = watch("buildingMode"); + const [buildingMode, firstDay, lastDay] = watch([ + "buildingMode", + "firstDay", + "lastDay", + ]); useEffect(() => { if (buildingMode === "Derated") { @@ -35,16 +40,34 @@ function Fields(props: Props) { } }, [buildingMode, setValue]); + useDebouncedEffect( + () => { + if (firstDay > 0 && firstDay > lastDay) { + setValue("lastDay", firstDay); + } + }, + { wait: 500, deps: [firstDay] } + ); + + useDebouncedEffect( + () => { + if (lastDay > 0 && lastDay < firstDay) { + setValue("firstDay", lastDay); + } + }, + { wait: 500, deps: [lastDay] } + ); + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// const handleDayValidation = (v: number) => { - if (v === 0 || Number.isNaN(v)) { + if (v < 1 || Number.isNaN(v)) { return "Minimum is 1"; } if (getValues("firstDay") > getValues("lastDay")) { - return "First day must be lower or equal to last day"; + return false; } if (getValues("leapYear")) { return v <= 366 ? true : "Maximum is 366 for a leap year"; @@ -155,7 +178,7 @@ function Fields(props: Props) { ? true : "Value must be 1 when building mode is derated"; } - if (v === 0) { + if (v < 1) { return "Minimum is 1"; } return v <= 50000 ? true : "Maximum is 50000"; @@ -226,16 +249,16 @@ function Fields(props: Props) { <> (); const { data, status, error } = usePromiseWithSnackbarError( () => getFormValues(study.id), - { errorMessage: "Cannot get study data" }, // TODO i18n - [study.id] + { errorMessage: "Cannot get study data", deps: [study.id] } // TODO i18n ); return R.cond([ diff --git a/webapp/src/components/singlestudy/explore/Configuration/General/styles.ts b/webapp/src/components/App/Singlestudy/explore/Configuration/General/styles.ts similarity index 84% rename from webapp/src/components/singlestudy/explore/Configuration/General/styles.ts rename to webapp/src/components/App/Singlestudy/explore/Configuration/General/styles.ts index 7a598c31e8..47e21d4fb3 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/General/styles.ts +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/styles.ts @@ -1,5 +1,5 @@ import { styled, experimental_sx as sx } from "@mui/material"; -import Fieldset from "../../../../common/Fieldset"; +import Fieldset from "../../../../../common/Fieldset"; export const StyledFieldset = styled(Fieldset)( sx({ diff --git a/webapp/src/components/singlestudy/explore/Configuration/General/utils.ts b/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts similarity index 97% rename from webapp/src/components/singlestudy/explore/Configuration/General/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts index fb5c104b17..e99b91e072 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/General/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/General/utils.ts @@ -1,6 +1,6 @@ import * as RA from "ramda-adjunct"; -import { StudyMetadata } from "../../../../../common/types"; -import { getStudyData } from "../../../../../services/api/study"; +import { StudyMetadata } from "../../../../../../common/types"; +import { getStudyData } from "../../../../../../services/api/study"; enum Month { January = "january", diff --git a/webapp/src/components/singlestudy/explore/Configuration/OptimizationPreferences/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/OptimizationPreferences/index.tsx similarity index 59% rename from webapp/src/components/singlestudy/explore/Configuration/OptimizationPreferences/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/OptimizationPreferences/index.tsx index 597bfb4e4c..dab08eabe8 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/OptimizationPreferences/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/OptimizationPreferences/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; function OptimizationPreferences() { return ; diff --git a/webapp/src/components/singlestudy/explore/Configuration/RegionalDistricts/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/RegionalDistricts/index.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Configuration/RegionalDistricts/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/RegionalDistricts/index.tsx index 310deea2ff..025121ed72 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/RegionalDistricts/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/RegionalDistricts/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; function RegionalDistricts() { return ; diff --git a/webapp/src/components/singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx similarity index 57% rename from webapp/src/components/singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx index c2715247a6..2957cbc159 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/TimeSeriesManagement/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; function TimeSeriesManagement() { return ; diff --git a/webapp/src/components/singlestudy/explore/Configuration/index.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/index.tsx similarity index 92% rename from webapp/src/components/singlestudy/explore/Configuration/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Configuration/index.tsx index cc6eb882d2..ff552b1fce 100644 --- a/webapp/src/components/singlestudy/explore/Configuration/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/index.tsx @@ -2,8 +2,8 @@ import { Paper } from "@mui/material"; import * as R from "ramda"; import { useMemo, useState } from "react"; -import PropertiesView from "../../../common/PropertiesView"; -import SplitLayoutView from "../../../common/SplitLayoutView"; +import PropertiesView from "../../../../common/PropertiesView"; +import SplitLayoutView from "../../../../common/SplitLayoutView"; import ListElement from "../common/ListElement"; import AdvancedParameters from "./AdvancedParameters"; import General from "./General"; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/AreaPropsView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreaPropsView.tsx similarity index 82% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/AreaPropsView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreaPropsView.tsx index 645ac340c8..b7a7953061 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/AreaPropsView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreaPropsView.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; -import { Area } from "../../../../../common/types"; -import PropertiesView from "../../../../common/PropertiesView"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getStudyAreas } from "../../../../../redux/selectors"; +import { Area } from "../../../../../../common/types"; +import PropertiesView from "../../../../../common/PropertiesView"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getStudyAreas } from "../../../../../../redux/selectors"; import ListElement from "../../common/ListElement"; -import { transformNameToId } from "../../../../../services/utils"; +import { transformNameToId } from "../../../../../../services/utils"; interface PropsType { studyId: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/AreasTab.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreasTab.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/AreasTab.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreasTab.tsx index 79d6a3f402..530a2792e8 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/AreasTab.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/AreasTab.tsx @@ -3,7 +3,7 @@ import { useMemo } from "react"; import { useOutletContext } from "react-router-dom"; import { Paper } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; import TabWrapper from "../../TabWrapper"; interface Props { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Hydro/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx similarity index 65% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Hydro/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx index b6da11eef8..030ffe91cc 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Hydro/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../../common/page/UnderConstruction"; import previewImage from "../Thermal/preview.png"; function Hydro() { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Load.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Load.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx index 392e722a22..4ce7112f71 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Load.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function Load() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/MiscGen.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx similarity index 66% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/MiscGen.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx index 6dbdb9106f..98fe2ca559 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/MiscGen.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function MiscGen() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx similarity index 93% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx index ca288efdce..74e8d27b8a 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/PropertiesForm.tsx @@ -2,15 +2,15 @@ import { Box, TextField, Typography } from "@mui/material"; import { AxiosError } from "axios"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { editStudy } from "../../../../../../services/api/study"; -import SelectFE from "../../../../../common/fieldEditors/SelectFE"; -import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; -import Fieldset from "../../../../../common/Fieldset"; -import { FormObj } from "../../../../../common/Form"; -import ColorPicker from "../../../../../common/fieldEditors/ColorPickerFE"; -import { stringToRGB } from "../../../../../common/fieldEditors/ColorPickerFE/utils"; +import { editStudy } from "../../../../../../../services/api/study"; +import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; +import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; +import Fieldset from "../../../../../../common/Fieldset"; +import { FormObj } from "../../../../../../common/Form"; +import ColorPicker from "../../../../../../common/fieldEditors/ColorPickerFE"; +import { stringToRGB } from "../../../../../../common/fieldEditors/ColorPickerFE/utils"; import { getPropertiesPath, PropertiesFields } from "./utils"; -import SwitchFE from "../../../../../common/fieldEditors/SwitchFE"; +import SwitchFE from "../../../../../../common/fieldEditors/SwitchFE"; export default function PropertiesForm( props: FormObj & { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/common.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/common.ts similarity index 87% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/common.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/common.ts index a71ef6437f..31d0adbc33 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/common.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/common.ts @@ -1,6 +1,8 @@ import { result } from "lodash"; -import usePromise, { PromiseStatus } from "../../../../../../hooks/usePromise"; -import { getStudyData } from "../../../../../../services/api/study"; +import usePromise, { + PromiseStatus, +} from "../../../../../../../hooks/usePromise"; +import { getStudyData } from "../../../../../../../services/api/study"; export interface FieldElement { path?: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/index.tsx similarity index 74% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/index.tsx index f583bf5ee7..bdddc0a461 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/index.tsx @@ -2,14 +2,16 @@ import * as R from "ramda"; import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useOutletContext } from "react-router"; -import { StudyMetadata } from "../../../../../../common/types"; -import usePromise, { PromiseStatus } from "../../../../../../hooks/usePromise"; -import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import Form from "../../../../../common/Form"; +import { StudyMetadata } from "../../../../../../../common/types"; +import usePromise, { + PromiseStatus, +} from "../../../../../../../hooks/usePromise"; +import useAppSelector from "../../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../../redux/selectors"; +import Form from "../../../../../../common/Form"; import PropertiesForm from "./PropertiesForm"; import { getDefaultValues, PropertiesFields } from "./utils"; -import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; function Properties() { const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -17,7 +19,6 @@ function Properties() { const [t] = useTranslation(); const { data: defaultValues, status } = usePromise( () => getDefaultValues(study.id, currentArea, t), - {}, [study.id, currentArea] ); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/utils.ts similarity index 95% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/utils.ts index 5586fe7018..7789f4a76f 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Properties/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/utils.ts @@ -1,7 +1,7 @@ import { FieldValues } from "react-hook-form"; import { TFunction } from "react-i18next"; -import { getStudyData } from "../../../../../../services/api/study"; -import { RGBToString } from "../../../../../common/fieldEditors/ColorPickerFE/utils"; +import { getStudyData } from "../../../../../../../services/api/study"; +import { RGBToString } from "../../../../../../common/fieldEditors/ColorPickerFE/utils"; export interface PropertiesType { ui: { diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/index.tsx new file mode 100644 index 0000000000..f2307f8ff0 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/index.tsx @@ -0,0 +1,8 @@ +import UnderConstruction from "../../../../../../common/page/UnderConstruction"; +import previewImage from "./preview.png"; + +function Renewables() { + return ; +} + +export default Renewables; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/preview.png b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/preview.png similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/preview.png rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/preview.png diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Reserve.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx similarity index 64% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Reserve.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx index f0d6ba1121..74c319e540 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Reserve.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function Reserve() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Solar.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Solar.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx index da25df93dc..debfe9f6cd 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Solar.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function Solar() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/index.tsx similarity index 65% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/index.tsx index 024ee1310b..12417d1c0c 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/index.tsx @@ -1,4 +1,4 @@ -import UnderConstruction from "../../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../../common/page/UnderConstruction"; import previewImage from "./preview.png"; function Thermal() { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/preview.png b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/preview.png similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Thermal/preview.png rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/preview.png diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Wind.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx similarity index 56% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Wind.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx index ad7368293d..6e82215308 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Wind.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx @@ -1,8 +1,8 @@ import { useOutletContext } from "react-router"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../common/types"; -import MatrixInput from "../../../../common/MatrixInput"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; +import MatrixInput from "../../../../../common/MatrixInput"; function Wind() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/index.tsx similarity index 81% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Areas/index.tsx index c7448e08f2..2d8b3fdeb1 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/index.tsx @@ -2,17 +2,17 @@ import { Box } from "@mui/material"; import * as R from "ramda"; import { ReactNode } from "react"; import { useOutletContext } from "react-router"; -import { StudyMetadata } from "../../../../../common/types"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import NoContent from "../../../../common/page/NoContent"; -import SplitLayoutView from "../../../../common/SplitLayoutView"; +import { StudyMetadata } from "../../../../../../common/types"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import NoContent from "../../../../../common/page/NoContent"; +import SplitLayoutView from "../../../../../common/SplitLayoutView"; import AreaPropsView from "./AreaPropsView"; import AreasTab from "./AreasTab"; import useStudyData from "../../hooks/useStudyData"; -import { getCurrentAreaId } from "../../../../../redux/selectors"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../../../../redux/hooks/useAppDispatch"; -import { setCurrentArea } from "../../../../../redux/ducks/studyDataSynthesis"; +import { getCurrentAreaId } from "../../../../../../redux/selectors"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../../../../redux/hooks/useAppDispatch"; +import { setCurrentArea } from "../../../../../../redux/ducks/studyDataSynthesis"; function Areas() { const { study } = useOutletContext<{ study: StudyMetadata }>(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/index.tsx similarity index 73% rename from webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/index.tsx index c107e441ba..d77e5c656f 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Areas/Renewables/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/index.tsx @@ -1,8 +1,8 @@ import UnderConstruction from "../../../../../common/page/UnderConstruction"; import previewImage from "./preview.png"; -function Renewables() { +function BindingConstraint() { return ; } -export default Renewables; +export default BindingConstraint; diff --git a/webapp/src/components/singlestudy/explore/Modelization/BindingConstraints/preview.png b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/preview.png similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/BindingConstraints/preview.png rename to webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/preview.png diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx similarity index 90% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx index ea5467b227..b01dca1d75 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyFileView.tsx @@ -6,11 +6,14 @@ import { useSnackbar } from "notistack"; import { useTranslation } from "react-i18next"; import { Box, Button } from "@mui/material"; import GetAppOutlinedIcon from "@mui/icons-material/GetAppOutlined"; -import { getStudyData, importFile } from "../../../../../../services/api/study"; +import { + getStudyData, + importFile, +} from "../../../../../../../services/api/study"; import { Header, Root, Content } from "./style"; -import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; -import ImportDialog from "../../../../../common/dialogs/ImportDialog"; +import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; +import ImportDialog from "../../../../../../common/dialogs/ImportDialog"; const logErr = debug("antares:createimportform:error"); diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx similarity index 92% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx index 1ce1ca3d70..37f4ac97e8 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyJsonView.tsx @@ -6,10 +6,13 @@ import { useTranslation } from "react-i18next"; import ReactJson from "react-json-view"; import SaveIcon from "@mui/icons-material/Save"; import { Box, Button, Typography } from "@mui/material"; -import { editStudy, getStudyData } from "../../../../../../services/api/study"; -import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import { + editStudy, + getStudyData, +} from "../../../../../../../services/api/study"; +import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; import { Header, Root, Content } from "./style"; -import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; interface PropTypes { data: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx similarity index 89% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx index f0dcf5d1c6..90b8120530 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/StudyMatrixView.tsx @@ -10,20 +10,23 @@ import GetAppOutlinedIcon from "@mui/icons-material/GetAppOutlined"; import { getStudyData, importFile, -} from "../../../../../../../services/api/study"; -import { MatrixType, MatrixEditDTO } from "../../../../../../../common/types"; +} from "../../../../../../../../services/api/study"; +import { + MatrixType, + MatrixEditDTO, +} from "../../../../../../../../common/types"; import { Header, Root, Content } from "../style"; -import usePromiseWithSnackbarError from "../../../../../../../hooks/usePromiseWithSnackbarError"; +import usePromiseWithSnackbarError from "../../../../../../../../hooks/usePromiseWithSnackbarError"; import { StyledButton } from "./style"; -import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; -import NoContent from "../../../../../../common/page/NoContent"; -import ImportDialog from "../../../../../../common/dialogs/ImportDialog"; -import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; -import EditableMatrix from "../../../../../../common/EditableMatrix"; +import useEnqueueErrorSnackbar from "../../../../../../../../hooks/useEnqueueErrorSnackbar"; +import NoContent from "../../../../../../../common/page/NoContent"; +import ImportDialog from "../../../../../../../common/dialogs/ImportDialog"; +import SimpleLoader from "../../../../../../../common/loaders/SimpleLoader"; +import EditableMatrix from "../../../../../../../common/EditableMatrix"; import { editMatrix, getStudyMatrixIndex, -} from "../../../../../../../services/api/matrix"; +} from "../../../../../../../../services/api/matrix"; const logErr = debug("antares:createimportform:error"); @@ -49,8 +52,8 @@ function StudyMatrixView(props: PropTypes) { () => getStudyMatrixIndex(study, formatedPath), { errorMessage: t("matrix.error.failedToRetrieveIndex"), - }, - [study, formatedPath] + deps: [study, formatedPath], + } ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/style.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/style.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/style.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/StudyMatrixView/style.ts diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx similarity index 95% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx index adadb250c0..8cc91350af 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/index.tsx @@ -4,7 +4,7 @@ import { Box } from "@mui/material"; import StudyFileView from "./StudyFileView"; import StudyJsonView from "./StudyJsonView"; import StudyMatrixView from "./StudyMatrixView/StudyMatrixView"; -import { StudyDataType } from "../../../../../../common/types"; +import { StudyDataType } from "../../../../../../../common/types"; interface PropTypes { study: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/style.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/style.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/style.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/style.ts diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/utils/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/utils/utils.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyDataView/utils/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyDataView/utils/utils.ts diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx index 65adba32ae..fc7e2157e5 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/index.tsx @@ -4,7 +4,7 @@ import { Box } from "@mui/material"; import { TreeItem, TreeView } from "@mui/lab"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; -import { StudyDataType } from "../../../../../../common/types"; +import { StudyDataType } from "../../../../../../../common/types"; import { getStudyParams } from "./utils"; interface ItemPropTypes { diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts similarity index 93% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts index 4d8e1f8520..ea0b7d6cb5 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/StudyTreeView/utils.ts @@ -1,7 +1,7 @@ import IntegrationInstructionsIcon from "@mui/icons-material/IntegrationInstructions"; import TextSnippetIcon from "@mui/icons-material/TextSnippet"; import { SvgIconComponent } from "@mui/icons-material"; -import { StudyDataType } from "../../../../../../common/types"; +import { StudyDataType } from "../../../../../../../common/types"; export interface StudyParams { type: StudyDataType; diff --git a/webapp/src/components/singlestudy/explore/Modelization/DebugView/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx similarity index 89% rename from webapp/src/components/singlestudy/explore/Modelization/DebugView/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx index 5d1b32fab7..987ffd151e 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/DebugView/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/DebugView/index.tsx @@ -4,12 +4,12 @@ import debug from "debug"; import { useTranslation } from "react-i18next"; import { useOutletContext } from "react-router-dom"; import { Box } from "@mui/material"; -import { getStudyData } from "../../../../../services/api/study"; +import { getStudyData } from "../../../../../../services/api/study"; import StudyTreeView from "./StudyTreeView"; import StudyDataView from "./StudyDataView"; -import { StudyDataType, StudyMetadata } from "../../../../../common/types"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; +import { StudyDataType, StudyMetadata } from "../../../../../../common/types"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; const logError = debug("antares:studyview:error"); @@ -44,7 +44,7 @@ function DebugView() { ); useEffect(() => { - if (study && !study.archived) { + if (study) { initStudyData(study.id); } }, [study, initStudyData]); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkPropsView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkPropsView.tsx similarity index 84% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkPropsView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkPropsView.tsx index 6f5c502e8c..fe3b605fe7 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkPropsView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkPropsView.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; -import PropertiesView from "../../../../common/PropertiesView"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import { getStudyLinks } from "../../../../../redux/selectors"; +import PropertiesView from "../../../../../common/PropertiesView"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import { getStudyLinks } from "../../../../../../redux/selectors"; import ListElement from "../../common/ListElement"; -import { LinkElement } from "../../../../../common/types"; +import { LinkElement } from "../../../../../../common/types"; interface PropsType { studyId: string; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx similarity index 93% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx index 58fcd7b9cc..14ac6a533b 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx @@ -2,19 +2,19 @@ import { Box } from "@mui/material"; import { AxiosError } from "axios"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { editStudy } from "../../../../../../services/api/study"; -import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; -import Fieldset from "../../../../../common/Fieldset"; -import { AutoSubmitHandler, FormObj } from "../../../../../common/Form"; +import { editStudy } from "../../../../../../../services/api/study"; +import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar"; +import Fieldset from "../../../../../../common/Fieldset"; +import { AutoSubmitHandler, FormObj } from "../../../../../../common/Form"; import { getLinkPath, LinkFields } from "./utils"; -import SwitchFE from "../../../../../common/fieldEditors/SwitchFE"; +import SwitchFE from "../../../../../../common/fieldEditors/SwitchFE"; import { LinkElement, MatrixStats, StudyMetadata, -} from "../../../../../../common/types"; -import SelectFE from "../../../../../common/fieldEditors/SelectFE"; -import MatrixInput from "../../../../../common/MatrixInput"; +} from "../../../../../../../common/types"; +import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; +import MatrixInput from "../../../../../../common/MatrixInput"; import LinkMatrixView from "./LinkMatrixView"; export default function LinkForm( diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx similarity index 95% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx index e8b4bea0bf..429092c5d9 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx @@ -5,8 +5,8 @@ import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; import { useTranslation } from "react-i18next"; -import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; -import MatrixInput from "../../../../../common/MatrixInput"; +import { MatrixStats, StudyMetadata } from "../../../../../../../common/types"; +import MatrixInput from "../../../../../../common/MatrixInput"; export const StyledTab = styled(Tabs)({ width: "98%", diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/index.tsx similarity index 80% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/index.tsx index a15ed3271e..f180bf152f 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/index.tsx @@ -1,12 +1,14 @@ import * as R from "ramda"; import { Box } from "@mui/material"; import { useOutletContext } from "react-router"; -import { LinkElement, StudyMetadata } from "../../../../../../common/types"; -import usePromise, { PromiseStatus } from "../../../../../../hooks/usePromise"; -import Form from "../../../../../common/Form"; +import { LinkElement, StudyMetadata } from "../../../../../../../common/types"; +import usePromise, { + PromiseStatus, +} from "../../../../../../../hooks/usePromise"; +import Form from "../../../../../../common/Form"; import LinkForm from "./LinkForm"; import { getDefaultValues, LinkFields } from "./utils"; -import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import SimpleLoader from "../../../../../../common/loaders/SimpleLoader"; interface Props { link: LinkElement; @@ -17,7 +19,6 @@ function LinkView(props: Props) { const { link } = props; const { data: defaultValues, status } = usePromise( () => getDefaultValues(study.id, link.area1, link.area2), - {}, [study.id, link.area1, link.area2] ); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/utils.ts similarity index 97% rename from webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/utils.ts rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/utils.ts index e0d887bc93..ee6458fced 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/LinkView/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/utils.ts @@ -1,5 +1,5 @@ import { FieldValues } from "react-hook-form"; -import { getStudyData } from "../../../../../../services/api/study"; +import { getStudyData } from "../../../../../../../services/api/study"; type TransCapacitiesType = "infinite" | "ignore" | "enabled"; type AssetType = "ac" | "dc" | "gaz" | "virt"; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Links/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/index.tsx similarity index 80% rename from webapp/src/components/singlestudy/explore/Modelization/Links/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Links/index.tsx index f5ecb359ef..f53f711817 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Links/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/index.tsx @@ -2,16 +2,19 @@ import { Box } from "@mui/material"; import * as R from "ramda"; import { ReactNode } from "react"; import { useOutletContext } from "react-router"; -import { StudyMetadata } from "../../../../../common/types"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import NoContent from "../../../../common/page/NoContent"; -import SplitLayoutView from "../../../../common/SplitLayoutView"; +import { StudyMetadata } from "../../../../../../common/types"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import NoContent from "../../../../../common/page/NoContent"; +import SplitLayoutView from "../../../../../common/SplitLayoutView"; import LinkPropsView from "./LinkPropsView"; import useStudyData from "../../hooks/useStudyData"; -import { getCurrentLinkId, selectLinks } from "../../../../../redux/selectors"; -import useAppSelector from "../../../../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../../../../redux/hooks/useAppDispatch"; -import { setCurrentLink } from "../../../../../redux/ducks/studyDataSynthesis"; +import { + getCurrentLinkId, + selectLinks, +} from "../../../../../../redux/selectors"; +import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../../../../redux/hooks/useAppDispatch"; +import { setCurrentLink } from "../../../../../../redux/ducks/studyDataSynthesis"; import LinkView from "./LinkView"; function Links() { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/CreateAreaModal.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/CreateAreaModal.tsx similarity index 93% rename from webapp/src/components/singlestudy/explore/Modelization/Map/CreateAreaModal.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/CreateAreaModal.tsx index 6407f528ae..5292da842a 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/CreateAreaModal.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/CreateAreaModal.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { Box, Button, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useSnackbar } from "notistack"; -import { isStringEmpty } from "../../../../../services/utils"; -import BasicDialog from "../../../../common/dialogs/BasicDialog"; +import { isStringEmpty } from "../../../../../../services/utils"; +import BasicDialog from "../../../../../common/dialogs/BasicDialog"; interface PropType { open: boolean; diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/GraphView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/GraphView.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Modelization/Map/GraphView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/GraphView.tsx index 8a03febe72..a811b33490 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/GraphView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/GraphView.tsx @@ -1,6 +1,6 @@ import { RefObject } from "react"; import { Graph, GraphLink, GraphNode } from "react-d3-graph"; -import { LinkProperties, NodeProperties } from "../../../../../common/types"; +import { LinkProperties, NodeProperties } from "../../../../../../common/types"; import NodeView from "./NodeView"; interface GraphViewProps { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/LinksView.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/LinksView.tsx index fb61e364b2..c34a4ed988 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/LinksView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/LinksView.tsx @@ -3,7 +3,7 @@ import { ListItemText, ListItem, Typography, Box, styled } from "@mui/material"; import { useTranslation } from "react-i18next"; import AutoSizer from "react-virtualized-auto-sizer"; import { areEqual, FixedSizeList, ListChildComponentProps } from "react-window"; -import { LinkProperties, NodeProperties } from "../../../../../common/types"; +import { LinkProperties, NodeProperties } from "../../../../../../common/types"; const ROW_ITEM_SIZE = 40; const BUTTONS_SIZE = 40; @@ -48,6 +48,8 @@ const Row = memo((props: ListChildComponentProps) => { ); }, areEqual); +Row.displayName = "Row"; + function LinksView(props: PropsType) { const { links, node, setSelectedItem } = props; const [t] = useTranslation(); diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/MapPropsView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/MapPropsView.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Modelization/Map/MapPropsView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/MapPropsView.tsx index 42246561f7..17f80fdfdd 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/MapPropsView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/MapPropsView.tsx @@ -5,9 +5,9 @@ import { NodeProperties, UpdateAreaUi, isNode, -} from "../../../../../common/types"; +} from "../../../../../../common/types"; import PanelView from "./PanelView"; -import PropertiesView from "../../../../common/PropertiesView"; +import PropertiesView from "../../../../../common/PropertiesView"; import ListElement from "../../common/ListElement"; interface PropsType { diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/NodeView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/NodeView.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Modelization/Map/NodeView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/NodeView.tsx index 719c453d51..a90de7f922 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/NodeView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/NodeView.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef } from "react"; import { Box, styled } from "@mui/material"; import AddLinkIcon from "@mui/icons-material/AddLink"; -import { NodeProperties } from "../../../../../common/types"; -import { rgbToHsl } from "../../../../../services/utils"; +import { NodeProperties } from "../../../../../../common/types"; +import { rgbToHsl } from "../../../../../../services/utils"; const nodeStyle = { opacity: ".9", diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/PanelView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/PanelView.tsx similarity index 98% rename from webapp/src/components/singlestudy/explore/Modelization/Map/PanelView.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/PanelView.tsx index cae6a2d3dd..114ea1b3ad 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/PanelView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/PanelView.tsx @@ -14,9 +14,9 @@ import { NodeProperties, LinkProperties, UpdateAreaUi, -} from "../../../../../common/types"; +} from "../../../../../../common/types"; import LinksView from "./LinksView"; -import ConfirmationDialog from "../../../../common/dialogs/ConfirmationDialog"; +import ConfirmationDialog from "../../../../../common/dialogs/ConfirmationDialog"; export const StyledDeleteIcon = styled(DeleteIcon)(({ theme }) => ({ cursor: "pointer", diff --git a/webapp/src/components/singlestudy/explore/Modelization/Map/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx similarity index 91% rename from webapp/src/components/singlestudy/explore/Modelization/Map/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx index c750c4949f..2cac0ab02f 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/Map/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/index.tsx @@ -12,27 +12,29 @@ import { NodeProperties, StudyMetadata, UpdateAreaUi, -} from "../../../../../common/types"; -import SplitLayoutView from "../../../../common/SplitLayoutView"; +} from "../../../../../../common/types"; +import SplitLayoutView from "../../../../../common/SplitLayoutView"; import { createArea, updateAreaUI, deleteArea, deleteLink, createLink, -} from "../../../../../services/api/studydata"; + getAllLinks, +} from "../../../../../../services/api/studydata"; import { getAreaPositions, getStudySynthesis, -} from "../../../../../services/api/study"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; +} from "../../../../../../services/api/study"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; import GraphView from "./GraphView"; import MapPropsView from "./MapPropsView"; import CreateAreaModal from "./CreateAreaModal"; -import mapbackground from "../../../../../assets/mapbackground.png"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import { setCurrentArea } from "../../../../../redux/ducks/studyDataSynthesis"; -import useAppDispatch from "../../../../../redux/hooks/useAppDispatch"; +import mapbackground from "./mapbackground.png"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import { setCurrentArea } from "../../../../../../redux/ducks/studyDataSynthesis"; +import useAppDispatch from "../../../../../../redux/hooks/useAppDispatch"; +import { linkStyle } from "./utils"; const FONT_SIZE = 16; const NODE_HEIGHT = 400; @@ -300,19 +302,19 @@ function Map() { }; }); setNodeData(tempNodeData); + const links = await getAllLinks({ uuid: study.id, withUi: true }); setLinkData( - Object.keys(data.areas).reduce( - (links, currentAreaId) => - links.concat( - Object.keys(data.areas[currentAreaId].links).map( - (linkId) => ({ - source: currentAreaId, - target: linkId, - }) - ) - ), - [] as Array - ) + links.map((link) => { + const [style, linecap] = linkStyle(link.ui?.style); + return { + source: link.area1, + target: link.area2, + color: `rgb(${link.ui?.color}`, + strokeDasharray: style, + strokeLinecap: linecap, + strokeWidth: link.ui?.width, + }; + }) ); } } catch (e) { diff --git a/webapp/src/assets/mapbackground.png b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/mapbackground.png similarity index 100% rename from webapp/src/assets/mapbackground.png rename to webapp/src/components/App/Singlestudy/explore/Modelization/Map/mapbackground.png diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Map/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/utils.ts new file mode 100644 index 0000000000..58cc038a00 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Map/utils.ts @@ -0,0 +1,38 @@ +import * as R from "ramda"; + +export const linkStyle = (linkStyle: string): Array | string> => { + const linkCond = R.cond([ + [ + R.equals("dot"), + (): Array | string> => { + return [[1, 5], "round"]; + }, + ], + [ + R.equals("dash"), + (): Array | string> => { + return [[16, 8], "square"]; + }, + ], + [ + R.equals("dotdash"), + (): Array | string> => { + return [[10, 6, 1, 6], "square"]; + }, + ], + [ + (_: string): boolean => true, + (): Array | string> => { + return [[0], "butt"]; + }, + ], + ]); + + const values = linkCond(linkStyle); + const style = values[0] as Array; + const linecap = values[1] as string; + + return [style, linecap]; +}; + +export default {}; diff --git a/webapp/src/components/singlestudy/explore/Modelization/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/index.tsx similarity index 95% rename from webapp/src/components/singlestudy/explore/Modelization/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Modelization/index.tsx index 288ff7bc3e..1fcdc0045f 100644 --- a/webapp/src/components/singlestudy/explore/Modelization/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/index.tsx @@ -3,7 +3,7 @@ import { useMemo } from "react"; import { useOutletContext } from "react-router-dom"; import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { StudyMetadata } from "../../../../common/types"; +import { StudyMetadata } from "../../../../../common/types"; import TabWrapper from "../TabWrapper"; function Modelization() { diff --git a/webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx similarity index 83% rename from webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index cf6dd1a054..850b450ff9 100644 --- a/webapp/src/components/singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -2,9 +2,9 @@ import { useNavigate, useOutletContext } from "react-router"; import { useTranslation } from "react-i18next"; import { Box, Button } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import UnderConstruction from "../../../../common/page/UnderConstruction"; +import UnderConstruction from "../../../../../common/page/UnderConstruction"; import previewImage from "./preview.png"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; function ResultDetails() { const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -14,8 +14,8 @@ function ResultDetails() { return ( { + const runningJobs: OutputDetail[] = jobs + .filter((job) => !job.completionDate) + .map((job) => { + return { + name: job.id, + creationDate: job.creationDate, + job, + }; + }); + const outputDetails = outputs.map((output) => { + const relatedJob = jobs.find((job) => job.outputId === output.name); + const outputDetail: OutputDetail = { + name: output.name, + }; + if (relatedJob) { + outputDetail.completionDate = relatedJob.completionDate; + outputDetail.creationDate = relatedJob.creationDate; + outputDetail.job = relatedJob; + } else { + const dateComponents = output.name.match( + "(\\d{4})(\\d{2})(\\d{2})-(\\d{2})(\\d{2}).*" + ); + if (dateComponents) { + outputDetail.completionDate = `${dateComponents[1]}-${dateComponents[2]}-${dateComponents[3]} ${dateComponents[4]}:${dateComponents[5]}`; + } + } + return outputDetail; + }); + return runningJobs.concat(outputDetails); +}; + +function Results() { + const { study } = useOutletContext<{ study: StudyMetadata }>(); + const { t } = useTranslation(); + const navigate = useNavigate(); + + const { data: studyJobs, isLoading: studyJobsLoading } = + usePromiseWithSnackbarError(() => getStudyJobs(study.id), { + errorMessage: t("results.error.jobs"), + deps: [study.id], + }); + + const { data: studyOutputs, isLoading: studyOutputsLoading } = + usePromiseWithSnackbarError(() => getStudyOutputs(study.id), { + errorMessage: t("results.error.outputs"), + deps: [study.id], + }); + + const outputs = useMemo(() => { + if (studyJobs && studyOutputs) { + return combineJobsAndOutputs(studyJobs, studyOutputs).sort((a, b) => { + if (!a.completionDate || !b.completionDate) { + if (!a.completionDate && !b.completionDate) { + return moment(a.creationDate).isAfter(moment(b.creationDate)) + ? -1 + : 1; + } + if (!a.completionDate) { + return -1; + } + return 1; + } + return moment(a.completionDate).isAfter(moment(b.completionDate)) + ? -1 + : 1; + }); + } + return []; + }, [studyJobs, studyOutputs]); + + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + const handleOutputNameClick = (outputName: string) => () => { + navigate(`/studies/${study.id}/explore/results/${outputName}`); + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ( + + + + + + {t("global.name")} + + + {t("global.date")} + + + {t("tasks.action")} + + + + {R.cond([ + [ + () => studyJobsLoading && studyOutputsLoading, + () => ( + <> + {Array.from({ length: 3 }, (v, k) => k).map((v) => ( + td, &:last-child > th": { + border: 0, + }, + }} + > + + + + + ))} + + ), + ], + [ + () => outputs.length > 0, + () => ( + <> + {outputs.map((row) => ( + td, &:last-child > th": { + border: 0, + }, + }} + > + + {row.completionDate ? ( + + {row.name} + + ) : ( + + + {row.name} + + )} + + + + {row.creationDate && ( + + + {convertUTCToLocalTime(row.creationDate)} + + )} + + {row.completionDate && ( + <> + + {convertUTCToLocalTime(row.completionDate)} + + )} + + + + + + + {row.completionDate && row.job ? ( + + { + if (row.job) { + downloadJobOutput(row.job.id); + } + }} + /> + + ) : ( + + )} + + {row.job && ( + + )} + + + + ))} + + ), + ], + [ + R.T, + () => ( + td, &:last-child > th": { + border: 0, + }, + }} + > + + + {t("results.noOutputs")} + + + + ), + ], + ])()} + +
+
+
+ ); +} + +export default Results; diff --git a/webapp/src/components/singlestudy/explore/TabWrapper.tsx b/webapp/src/components/App/Singlestudy/explore/TabWrapper.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/TabWrapper.tsx rename to webapp/src/components/App/Singlestudy/explore/TabWrapper.tsx index ed41c8e49c..d8b43793ba 100644 --- a/webapp/src/components/singlestudy/explore/TabWrapper.tsx +++ b/webapp/src/components/App/Singlestudy/explore/TabWrapper.tsx @@ -6,7 +6,7 @@ import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; -import { StudyMetadata } from "../../../common/types"; +import { StudyMetadata } from "../../../../common/types"; export const StyledTab = styled(Tabs, { shouldForwardProp: (prop) => prop !== "border" && prop !== "tabStyle", diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx similarity index 97% rename from webapp/src/components/singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx index 8fcaf98927..578e969158 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CandidateForm.tsx @@ -11,7 +11,7 @@ import { import { useTranslation } from "react-i18next"; import SaveIcon from "@mui/icons-material/Save"; import DeleteIcon from "@mui/icons-material/Delete"; -import ConfirmationDialog from "../../../../common/dialogs/ConfirmationDialog"; +import ConfirmationDialog from "../../../../../common/dialogs/ConfirmationDialog"; import { Title, Fields, @@ -21,13 +21,13 @@ import { StyledVisibilityIcon, StyledDeleteIcon, } from "../share/styles"; -import { LinkCreationInfo } from "../../../../../common/types"; +import { LinkCreationInfoDTO } from "../../../../../../common/types"; import { XpansionCandidate } from "../types"; -import SelectSingle from "../../../../common/SelectSingle"; +import SelectSingle from "../../../../../common/SelectSingle"; interface PropType { candidate: XpansionCandidate | undefined; - links: Array; + links: Array; capacities: Array; deleteCandidate: (name: string | undefined) => Promise; updateCandidate: ( diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx similarity index 95% rename from webapp/src/components/singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx index 072efbc5d2..a64c033498 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/CreateCandidateDialog.tsx @@ -1,15 +1,15 @@ import { useState } from "react"; import { TextField, Button, Box, Divider, ButtonGroup } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { LinkCreationInfo } from "../../../../../common/types"; +import { LinkCreationInfoDTO } from "../../../../../../common/types"; import { XpansionCandidate } from "../types"; -import SelectSingle from "../../../../common/SelectSingle"; +import SelectSingle from "../../../../../common/SelectSingle"; import { HoverButton, ActiveButton } from "../share/styles"; -import BasicDialog from "../../../../common/dialogs/BasicDialog"; +import BasicDialog from "../../../../../common/dialogs/BasicDialog"; interface PropType { open: boolean; - links: Array; + links: Array; onClose: () => void; onSave: (candidate: XpansionCandidate) => void; } diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx similarity index 96% rename from webapp/src/components/singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx index e33b5fad65..f03ad1c4d0 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/XpansionPropsView.tsx @@ -2,9 +2,9 @@ import { useState } from "react"; import { Box, Button } from "@mui/material"; import { useTranslation } from "react-i18next"; import DeleteIcon from "@mui/icons-material/Delete"; -import PropertiesView from "../../../../common/PropertiesView"; +import PropertiesView from "../../../../../common/PropertiesView"; import { XpansionCandidate } from "../types"; -import ConfirmationDialog from "../../../../common/dialogs/ConfirmationDialog"; +import ConfirmationDialog from "../../../../../common/dialogs/ConfirmationDialog"; import ListElement from "../../common/ListElement"; interface PropsType { diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx similarity index 90% rename from webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx index 5eb1ece05b..104f89858b 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Candidates/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Candidates/index.tsx @@ -6,9 +6,9 @@ import { useTranslation } from "react-i18next"; import { Backdrop, Box, CircularProgress } from "@mui/material"; import { usePromise as usePromiseWrapper } from "react-use"; import { useSnackbar } from "notistack"; -import { MatrixType, StudyMetadata } from "../../../../../common/types"; +import { MatrixType, StudyMetadata } from "../../../../../../common/types"; import { XpansionCandidate } from "../types"; -import SplitLayoutView from "../../../../common/SplitLayoutView"; +import SplitLayoutView from "../../../../../common/SplitLayoutView"; import { getAllCandidates, getAllCapacities, @@ -18,18 +18,18 @@ import { updateCandidate, getCapacity, xpansionConfigurationExist, -} from "../../../../../services/api/xpansion"; +} from "../../../../../../services/api/xpansion"; import { transformNameToId, removeEmptyFields, -} from "../../../../../services/utils/index"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import { getAllLinks } from "../../../../../services/api/studydata"; +} from "../../../../../../services/utils/index"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import { getAllLinks } from "../../../../../../services/api/studydata"; import XpansionPropsView from "./XpansionPropsView"; import CreateCandidateDialog from "./CreateCandidateDialog"; import CandidateForm from "./CandidateForm"; -import usePromiseWithSnackbarError from "../../../../../hooks/usePromiseWithSnackbarError"; -import DataViewerDialog from "../../../../common/dialogs/DataViewerDialog"; +import usePromiseWithSnackbarError from "../../../../../../hooks/usePromiseWithSnackbarError"; +import DataViewerDialog from "../../../../../common/dialogs/DataViewerDialog"; function Candidates() { const [t] = useTranslation(); @@ -76,8 +76,8 @@ function Candidates() { { errorMessage: t("xpansion.error.loadConfiguration"), resetDataOnReload: false, - }, - [study] + deps: [study], + } ); const { data: capaLinks } = usePromiseWithSnackbarError( @@ -89,13 +89,12 @@ function Candidates() { if (exist) { return { capacities: await getAllCapacities(study.id), - links: await getAllLinks(study.id), + links: await getAllLinks({ uuid: study.id }), }; } return {}; }, - { errorMessage: t("xpansion.error.loadConfiguration") }, - [study] + { errorMessage: t("xpansion.error.loadConfiguration"), deps: [study] } ); const deleteXpansion = async () => { diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Capacities/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Capacities/index.tsx similarity index 88% rename from webapp/src/components/singlestudy/explore/Xpansion/Capacities/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Capacities/index.tsx index 478ef30468..57722517b1 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Capacities/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Capacities/index.tsx @@ -4,17 +4,17 @@ import { useOutletContext } from "react-router-dom"; import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; import { Box, Paper } from "@mui/material"; -import { MatrixType, StudyMetadata } from "../../../../../common/types"; +import { MatrixType, StudyMetadata } from "../../../../../../common/types"; import { getAllCapacities, deleteCapacity, getCapacity, addCapacity, -} from "../../../../../services/api/xpansion"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import DataViewerDialog from "../../../../common/dialogs/DataViewerDialog"; -import FileTable from "../../../../common/FileTable"; +} from "../../../../../../services/api/xpansion"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import DataViewerDialog from "../../../../../common/dialogs/DataViewerDialog"; +import FileTable from "../../../../../common/FileTable"; import { Title } from "../share/styles"; function Capacities() { diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Files/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Files/index.tsx similarity index 88% rename from webapp/src/components/singlestudy/explore/Xpansion/Files/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Files/index.tsx index 6a8e15ed7d..6ade41131d 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Files/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Files/index.tsx @@ -4,17 +4,17 @@ import { useOutletContext } from "react-router-dom"; import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; import { Box, Paper } from "@mui/material"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; import { getAllConstraints, deleteConstraints, getConstraint, addConstraints, -} from "../../../../../services/api/xpansion"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import FileTable from "../../../../common/FileTable"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import DataViewerDialog from "../../../../common/dialogs/DataViewerDialog"; +} from "../../../../../../services/api/xpansion"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import FileTable from "../../../../../common/FileTable"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import DataViewerDialog from "../../../../../common/dialogs/DataViewerDialog"; import { Title } from "../share/styles"; function Files() { diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Settings/SettingsForm.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx similarity index 99% rename from webapp/src/components/singlestudy/explore/Xpansion/Settings/SettingsForm.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx index b6c867a879..c42cc78323 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Settings/SettingsForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/SettingsForm.tsx @@ -9,7 +9,7 @@ import { Title, StyledVisibilityIcon, } from "../share/styles"; -import SelectSingle from "../../../../common/SelectSingle"; +import SelectSingle from "../../../../../common/SelectSingle"; interface PropType { settings: XpansionSettings; diff --git a/webapp/src/components/singlestudy/explore/Xpansion/Settings/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx similarity index 90% rename from webapp/src/components/singlestudy/explore/Xpansion/Settings/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx index 65a8cf842e..721d50ca62 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/Settings/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/Settings/index.tsx @@ -5,19 +5,19 @@ import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; import { Box, Paper } from "@mui/material"; import { useSnackbar } from "notistack"; -import { StudyMetadata } from "../../../../../common/types"; +import { StudyMetadata } from "../../../../../../common/types"; import { XpansionSettings } from "../types"; import { getXpansionSettings, getAllConstraints, getConstraint, updateXpansionSettings, -} from "../../../../../services/api/xpansion"; +} from "../../../../../../services/api/xpansion"; import SettingsForm from "./SettingsForm"; -import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; -import SimpleLoader from "../../../../common/loaders/SimpleLoader"; -import { removeEmptyFields } from "../../../../../services/utils/index"; -import DataViewerDialog from "../../../../common/dialogs/DataViewerDialog"; +import useEnqueueErrorSnackbar from "../../../../../../hooks/useEnqueueErrorSnackbar"; +import SimpleLoader from "../../../../../common/loaders/SimpleLoader"; +import { removeEmptyFields } from "../../../../../../services/utils/index"; +import DataViewerDialog from "../../../../../common/dialogs/DataViewerDialog"; function Settings() { const [t] = useTranslation(); diff --git a/webapp/src/components/singlestudy/explore/Xpansion/index.tsx b/webapp/src/components/App/Singlestudy/explore/Xpansion/index.tsx similarity index 93% rename from webapp/src/components/singlestudy/explore/Xpansion/index.tsx rename to webapp/src/components/App/Singlestudy/explore/Xpansion/index.tsx index 96ec4f48d0..d36b29e1ed 100644 --- a/webapp/src/components/singlestudy/explore/Xpansion/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Xpansion/index.tsx @@ -4,12 +4,12 @@ import { AxiosError } from "axios"; import { useOutletContext } from "react-router-dom"; import { Box, Button } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { StudyMetadata } from "../../../../common/types"; +import { StudyMetadata } from "../../../../../common/types"; import { createXpansionConfiguration, xpansionConfigurationExist, -} from "../../../../services/api/xpansion"; -import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +} from "../../../../../services/api/xpansion"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; import TabWrapper from "../TabWrapper"; function Xpansion() { diff --git a/webapp/src/components/singlestudy/explore/Xpansion/share/styles.ts b/webapp/src/components/App/Singlestudy/explore/Xpansion/share/styles.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Xpansion/share/styles.ts rename to webapp/src/components/App/Singlestudy/explore/Xpansion/share/styles.ts diff --git a/webapp/src/components/singlestudy/explore/Xpansion/types.ts b/webapp/src/components/App/Singlestudy/explore/Xpansion/types.ts similarity index 100% rename from webapp/src/components/singlestudy/explore/Xpansion/types.ts rename to webapp/src/components/App/Singlestudy/explore/Xpansion/types.ts diff --git a/webapp/src/components/singlestudy/explore/common/ListElement.tsx b/webapp/src/components/App/Singlestudy/explore/common/ListElement.tsx similarity index 100% rename from webapp/src/components/singlestudy/explore/common/ListElement.tsx rename to webapp/src/components/App/Singlestudy/explore/common/ListElement.tsx diff --git a/webapp/src/components/singlestudy/explore/hooks/useStudyData.ts b/webapp/src/components/App/Singlestudy/explore/hooks/useStudyData.ts similarity index 76% rename from webapp/src/components/singlestudy/explore/hooks/useStudyData.ts rename to webapp/src/components/App/Singlestudy/explore/hooks/useStudyData.ts index 76acc71792..5dd59dd20e 100644 --- a/webapp/src/components/singlestudy/explore/hooks/useStudyData.ts +++ b/webapp/src/components/App/Singlestudy/explore/hooks/useStudyData.ts @@ -2,11 +2,11 @@ import { useEffect, useState } from "react"; import { FileStudyTreeConfigDTO, StudyMetadata, -} from "../../../../common/types"; -import { createStudyData } from "../../../../redux/ducks/studyDataSynthesis"; -import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; -import useAppSelector from "../../../../redux/hooks/useAppSelector"; -import { getStudyData } from "../../../../redux/selectors"; +} from "../../../../../common/types"; +import { createStudyData } from "../../../../../redux/ducks/studyDataSynthesis"; +import useAppDispatch from "../../../../../redux/hooks/useAppDispatch"; +import useAppSelector from "../../../../../redux/hooks/useAppSelector"; +import { getStudyData } from "../../../../../redux/selectors"; interface Props { studyId: StudyMetadata["id"]; diff --git a/webapp/src/pages/SingleStudy/index.tsx b/webapp/src/components/App/Singlestudy/index.tsx similarity index 85% rename from webapp/src/pages/SingleStudy/index.tsx rename to webapp/src/components/App/Singlestudy/index.tsx index 3a13304781..6621b23c2c 100644 --- a/webapp/src/pages/SingleStudy/index.tsx +++ b/webapp/src/components/App/Singlestudy/index.tsx @@ -11,21 +11,21 @@ import { VariantTree, WSEvent, WSMessage, -} from "../../common/types"; -import { getStudyMetadata } from "../../services/api/study"; -import NavHeader from "../../components/singlestudy/NavHeader"; +} from "../../../common/types"; +import { getStudyMetadata } from "../../../services/api/study"; +import NavHeader from "./NavHeader"; import { getVariantChildren, getVariantParents, -} from "../../services/api/variant"; -import TabWrapper from "../../components/singlestudy/explore/TabWrapper"; -import HomeView from "../../components/singlestudy/HomeView"; -import { setCurrentStudy } from "../../redux/ducks/studies"; -import { findNodeInTree } from "../../services/utils"; -import CommandDrawer from "../../components/singlestudy/Commands"; -import { addWsMessageListener } from "../../services/webSockets"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import SimpleLoader from "../../components/common/loaders/SimpleLoader"; +} from "../../../services/api/variant"; +import TabWrapper from "./explore/TabWrapper"; +import HomeView from "./HomeView"; +import { setCurrentStudy } from "../../../redux/ducks/studies"; +import { findNodeInTree } from "../../../services/utils"; +import CommandDrawer from "./Commands"; +import { addWsMessageListener } from "../../../services/webSockets"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import SimpleLoader from "../../common/loaders/SimpleLoader"; const logError = debug("antares:singlestudy:error"); diff --git a/webapp/src/components/studies/BatchModeMenu.tsx b/webapp/src/components/App/Studies/BatchModeMenu.tsx similarity index 100% rename from webapp/src/components/studies/BatchModeMenu.tsx rename to webapp/src/components/App/Studies/BatchModeMenu.tsx diff --git a/webapp/src/components/studies/CreateStudyDialog/index.tsx b/webapp/src/components/App/Studies/CreateStudyDialog/index.tsx similarity index 86% rename from webapp/src/components/studies/CreateStudyDialog/index.tsx rename to webapp/src/components/App/Studies/CreateStudyDialog/index.tsx index 566e2e78f1..d8c32e7d45 100644 --- a/webapp/src/components/studies/CreateStudyDialog/index.tsx +++ b/webapp/src/components/App/Studies/CreateStudyDialog/index.tsx @@ -6,21 +6,25 @@ import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; import { usePromise } from "react-use"; import * as R from "ramda"; -import SingleSelect from "../../common/SelectSingle"; -import MultiSelect from "../../common/SelectMulti"; -import { GenericInfo, GroupDTO, StudyPublicMode } from "../../../common/types"; -import TextSeparator from "../../common/TextSeparator"; -import { getGroups } from "../../../services/api/user"; -import TagTextInput from "../../common/TagTextInput"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import SingleSelect from "../../../common/SelectSingle"; +import MultiSelect from "../../../common/SelectMulti"; +import { + GenericInfo, + GroupDTO, + StudyPublicMode, +} from "../../../../common/types"; +import TextSeparator from "../../../common/TextSeparator"; +import { getGroups } from "../../../../services/api/user"; +import TagTextInput from "../../../common/TagTextInput"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; import BasicDialog, { BasicDialogProps, -} from "../../common/dialogs/BasicDialog"; +} from "../../../common/dialogs/BasicDialog"; import { Root, ElementContainer, InputElement } from "./style"; -import { createStudy } from "../../../redux/ducks/studies"; -import { getStudyVersionsFormatted } from "../../../redux/selectors"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import { createStudy } from "../../../../redux/ducks/studies"; +import { getStudyVersionsFormatted } from "../../../../redux/selectors"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; const logErr = debug("antares:createstudyform:error"); diff --git a/webapp/src/components/studies/CreateStudyDialog/style.ts b/webapp/src/components/App/Studies/CreateStudyDialog/style.ts similarity index 100% rename from webapp/src/components/studies/CreateStudyDialog/style.ts rename to webapp/src/components/App/Studies/CreateStudyDialog/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx similarity index 94% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx index 183eaddceb..d36463a8a8 100644 --- a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/index.tsx @@ -1,8 +1,8 @@ import { Box, Chip, ListItem } from "@mui/material"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import SelectSingle from "../../../../../common/SelectSingle"; -import TextSeparator from "../../../../../common/TextSeparator"; +import SelectSingle from "../../../../../../common/SelectSingle"; +import TextSeparator from "../../../../../../common/TextSeparator"; import { AddIcon } from "../../TagSelect/style"; import { FilterLinkContainer, Root, Container } from "./style"; diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/style.ts b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/style.ts similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/style.ts rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/MultipleLinkElement/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx similarity index 94% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx index bc3b164de1..0cef1d0190 100644 --- a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/index.tsx @@ -1,7 +1,7 @@ import { Box, TextField } from "@mui/material"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import TextSeparator from "../../../../../common/TextSeparator"; +import TextSeparator from "../../../../../../common/TextSeparator"; import { Root } from "./style"; interface FilterLink { diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/style.ts b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/style.ts similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/SingleLinkElement/style.ts rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/SingleLinkElement/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/index.tsx similarity index 96% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/index.tsx index e5f9ad7e60..549e2ea898 100644 --- a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/index.tsx @@ -5,8 +5,8 @@ import { Area, Set, StudyOutputDownloadType, -} from "../../../../../common/types"; -import SelectMulti from "../../../../common/SelectMulti"; +} from "../../../../../../common/types"; +import SelectMulti from "../../../../../common/SelectMulti"; import { Root } from "./style"; import MultipleLinkElement from "./MultipleLinkElement"; import SingleLinkElement from "./SingleLinkElement"; diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/Filter/style.ts b/webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/style.ts similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/Filter/style.ts rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/Filter/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/TagSelect/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/TagSelect/index.tsx similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/TagSelect/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/TagSelect/index.tsx diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/TagSelect/style.ts b/webapp/src/components/App/Studies/ExportModal/ExportFilter/TagSelect/style.ts similarity index 100% rename from webapp/src/components/studies/ExportModal/ExportFilter/TagSelect/style.ts rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/TagSelect/style.ts diff --git a/webapp/src/components/studies/ExportModal/ExportFilter/index.tsx b/webapp/src/components/App/Studies/ExportModal/ExportFilter/index.tsx similarity index 97% rename from webapp/src/components/studies/ExportModal/ExportFilter/index.tsx rename to webapp/src/components/App/Studies/ExportModal/ExportFilter/index.tsx index 1303990390..d79d350598 100644 --- a/webapp/src/components/studies/ExportModal/ExportFilter/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/ExportFilter/index.tsx @@ -10,11 +10,11 @@ import { StudyOutputDownloadDTO, StudyOutputDownloadLevelDTO, StudyOutputDownloadType, -} from "../../../../common/types"; +} from "../../../../../common/types"; import Filter from "./Filter"; import TagSelect from "./TagSelect"; -import SelectSingle from "../../../common/SelectSingle"; -import SelectMulti from "../../../common/SelectMulti"; +import SelectSingle from "../../../../common/SelectSingle"; +import SelectMulti from "../../../../common/SelectMulti"; const Root = styled(Box)(({ theme }) => ({ flex: 1, diff --git a/webapp/src/components/studies/ExportModal/index.tsx b/webapp/src/components/App/Studies/ExportModal/index.tsx similarity index 95% rename from webapp/src/components/studies/ExportModal/index.tsx rename to webapp/src/components/App/Studies/ExportModal/index.tsx index f81474eb0d..f8ad79a676 100644 --- a/webapp/src/components/studies/ExportModal/index.tsx +++ b/webapp/src/components/App/Studies/ExportModal/index.tsx @@ -14,19 +14,19 @@ import { StudyOutputDownloadDTO, StudyOutputDownloadLevelDTO, StudyOutputDownloadType, -} from "../../../common/types"; +} from "../../../../common/types"; import BasicDialog, { BasicDialogProps, -} from "../../common/dialogs/BasicDialog"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import SelectSingle from "../../common/SelectSingle"; +} from "../../../common/dialogs/BasicDialog"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import SelectSingle from "../../../common/SelectSingle"; import { exportStudy, exportOuput as callExportOutput, getStudyOutputs, getStudySynthesis, downloadOutput, -} from "../../../services/api/study"; +} from "../../../../services/api/study"; import ExportFilter from "./ExportFilter"; const logError = debug("antares:studies:card:error"); diff --git a/webapp/src/components/studies/FilterDrawer.tsx b/webapp/src/components/App/Studies/FilterDrawer.tsx similarity index 92% rename from webapp/src/components/studies/FilterDrawer.tsx rename to webapp/src/components/App/Studies/FilterDrawer.tsx index d8e1a9cb53..be3cd70953 100644 --- a/webapp/src/components/studies/FilterDrawer.tsx +++ b/webapp/src/components/App/Studies/FilterDrawer.tsx @@ -12,18 +12,18 @@ import { Typography, } from "@mui/material"; import { useEffect, useRef } from "react"; -import { STUDIES_FILTER_WIDTH } from "../../theme"; -import useAppSelector from "../../redux/hooks/useAppSelector"; +import { STUDIES_FILTER_WIDTH } from "../../../theme"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; import { getGroups, getStudyFilters, getStudyVersions, getUsers, -} from "../../redux/selectors"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import { StudyFilters, updateStudyFilters } from "../../redux/ducks/studies"; -import CheckboxesTagsFE from "../common/fieldEditors/CheckboxesTagsFE"; -import { displayVersionName } from "../../services/utils"; +} from "../../../redux/selectors"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import { StudyFilters, updateStudyFilters } from "../../../redux/ducks/studies"; +import CheckboxesTagsFE from "../../common/fieldEditors/CheckboxesTagsFE"; +import { displayVersionName } from "../../../services/utils"; interface Props { open: boolean; diff --git a/webapp/src/components/studies/HeaderBottom.tsx b/webapp/src/components/App/Studies/HeaderBottom.tsx similarity index 91% rename from webapp/src/components/studies/HeaderBottom.tsx rename to webapp/src/components/App/Studies/HeaderBottom.tsx index b806796444..d62184c9ef 100644 --- a/webapp/src/components/studies/HeaderBottom.tsx +++ b/webapp/src/components/App/Studies/HeaderBottom.tsx @@ -9,13 +9,13 @@ import { import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined"; import { useTranslation } from "react-i18next"; import { indigo, purple } from "@mui/material/colors"; -import useDebounce from "../../hooks/useDebounce"; -import useAppSelector from "../../redux/hooks/useAppSelector"; -import { getGroups, getStudyFilters, getUsers } from "../../redux/selectors"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import { StudyFilters, updateStudyFilters } from "../../redux/ducks/studies"; -import { GroupDTO, UserDTO } from "../../common/types"; -import { displayVersionName } from "../../services/utils"; +import useDebounce from "../../../hooks/useDebounce"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getGroups, getStudyFilters, getUsers } from "../../../redux/selectors"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import { StudyFilters, updateStudyFilters } from "../../../redux/ducks/studies"; +import { GroupDTO, UserDTO } from "../../../common/types"; +import { displayVersionName } from "../../../services/utils"; type PropTypes = { onOpenFilterClick: VoidFunction; diff --git a/webapp/src/components/studies/HeaderTopRight.tsx b/webapp/src/components/App/Studies/HeaderTopRight.tsx similarity index 90% rename from webapp/src/components/studies/HeaderTopRight.tsx rename to webapp/src/components/App/Studies/HeaderTopRight.tsx index 7906961264..fdeec318b1 100644 --- a/webapp/src/components/studies/HeaderTopRight.tsx +++ b/webapp/src/components/App/Studies/HeaderTopRight.tsx @@ -5,11 +5,11 @@ import { useTranslation } from "react-i18next"; import AddCircleOutlineOutlinedIcon from "@mui/icons-material/AddCircleOutlineOutlined"; import GetAppOutlinedIcon from "@mui/icons-material/GetAppOutlined"; import { useSnackbar } from "notistack"; -import { createStudy } from "../../redux/ducks/studies"; -import ImportDialog from "../common/dialogs/ImportDialog"; +import { createStudy } from "../../../redux/ducks/studies"; +import ImportDialog from "../../common/dialogs/ImportDialog"; import CreateStudyDialog from "./CreateStudyDialog"; -import useAppDispatch from "../../redux/hooks/useAppDispatch"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; function HeaderRight() { const [openCreateDialog, setOpenCreateDialog] = useState(false); diff --git a/webapp/src/components/studies/LauncherDialog.tsx b/webapp/src/components/App/Studies/LauncherDialog.tsx similarity index 92% rename from webapp/src/components/studies/LauncherDialog.tsx rename to webapp/src/components/App/Studies/LauncherDialog.tsx index a9584e6bd7..125ab569d3 100644 --- a/webapp/src/components/studies/LauncherDialog.tsx +++ b/webapp/src/components/App/Studies/LauncherDialog.tsx @@ -21,12 +21,12 @@ import { useSnackbar } from "notistack"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useMountedState } from "react-use"; import { shallowEqual } from "react-redux"; -import { StudyMetadata } from "../../common/types"; -import { LaunchOptions, launchStudy } from "../../services/api/study"; -import useEnqueueErrorSnackbar from "../../hooks/useEnqueueErrorSnackbar"; -import BasicDialog from "../common/dialogs/BasicDialog"; -import useAppSelector from "../../redux/hooks/useAppSelector"; -import { getStudy } from "../../redux/selectors"; +import { StudyMetadata } from "../../../common/types"; +import { LaunchOptions, launchStudy } from "../../../services/api/study"; +import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import BasicDialog from "../../common/dialogs/BasicDialog"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getStudy } from "../../../redux/selectors"; const LAUNCH_DURATION_MAX_HOURS = 240; @@ -171,7 +171,7 @@ function LauncherDialog(props: Props) { ) : ( {studyNames.map((name) => ( - + {name} ))} @@ -256,17 +256,6 @@ function LauncherDialog(props: Props) { width: "100%", }} > - { - handleChange("archive_output", checked); - }} - /> - } - label={t("study.archiveOutputMode") as string} - /> ( } ); +StudyCardCell.displayName = "StudyCardCell"; + export default StudyCardCell; diff --git a/webapp/src/components/studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx similarity index 95% rename from webapp/src/components/studies/StudiesList/index.tsx rename to webapp/src/components/App/Studies/StudiesList/index.tsx index cad9078db7..cb329ab4cf 100644 --- a/webapp/src/components/studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -24,27 +24,27 @@ import FolderOffIcon from "@mui/icons-material/FolderOff"; import RefreshIcon from "@mui/icons-material/Refresh"; import { FixedSizeGrid, GridOnScrollProps } from "react-window"; import { v4 as uuidv4 } from "uuid"; -import { StudyMetadata } from "../../../common/types"; +import { StudyMetadata } from "../../../../common/types"; import { STUDIES_HEIGHT_HEADER, STUDIES_LIST_HEADER_HEIGHT, -} from "../../../theme"; +} from "../../../../theme"; import { fetchStudies, setStudyScrollPosition, StudiesSortConf, updateStudiesSortConf, updateStudyFilters, -} from "../../../redux/ducks/studies"; +} from "../../../../redux/ducks/studies"; import LauncherDialog from "../LauncherDialog"; -import useDebounce from "../../../hooks/useDebounce"; +import useDebounce from "../../../../hooks/useDebounce"; import { getStudiesScrollPosition, getStudiesSortConf, getStudyFilters, -} from "../../../redux/selectors"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +} from "../../../../redux/selectors"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; import StudyCardCell from "./StudyCardCell"; import BatchModeMenu from "../BatchModeMenu"; @@ -123,8 +123,7 @@ function StudiesList(props: StudiesListProps) { (scrollProp: GridOnScrollProps) => { dispatch(setStudyScrollPosition(scrollProp.scrollTop)); }, - 400, - { trailing: true } + { wait: 400, trailing: true } ); const handleToggleSelectStudy = useCallback((sid: string) => { @@ -223,7 +222,7 @@ function StudiesList(props: StudiesListProps) { ({`${studyIds.length} ${t("global.studies").toLowerCase()}`}) - + { const handleCopyClick = () => { studyApi - .copyStudy(id, `${study?.name} (${t("study.copyId")})`, false) + .copyStudy(id, `${study?.name} (${t("studies.copySuffix")})`, false) .catch((err) => { enqueueErrorSnackbar(t("studies.error.copyStudy"), err); logError("Failed to copy study", study, err); @@ -369,20 +369,12 @@ const StudyCard = memo((props: Props) => {
- {study.archived ? ( - - ) : ( - - - - )} + +