diff --git a/antarest/study/model.py b/antarest/study/model.py index 207662aeea..b8378aa356 100644 --- a/antarest/study/model.py +++ b/antarest/study/model.py @@ -19,7 +19,7 @@ from pathlib import Path from antares.study.version import StudyVersion -from pydantic import BeforeValidator, PlainSerializer, computed_field, field_validator +from pydantic import BeforeValidator, PlainSerializer, field_validator from sqlalchemy import ( # type: ignore Boolean, Column, @@ -323,7 +323,7 @@ class StudyFolder: groups: t.List[Group] -class NonStudyFolderDTO(AntaresBaseModel): +class NonStudyFolder(AntaresBaseModel): """ DTO used by the explorer to list directories that aren't studies directory, this will be usefull for the front so the user can navigate in the hierarchy @@ -333,19 +333,6 @@ class NonStudyFolderDTO(AntaresBaseModel): workspace: str name: str - @computed_field(alias="parentPath") - def parent_path(self) -> Path: - """ - This computed field is convenient for the front. - - This field is also aliased as parentPath to match the front-end naming convention. - - Returns: the parent path of the current directory. Starting with the workspace as a root directory (we want /workspafe/folder1/sub... and not workspace/folder1/fsub... ). - """ - workspace_path = Path(f"/{self.workspace}") - full_path = workspace_path.joinpath(self.path) - return full_path.parent - class WorkspaceMetadata(AntaresBaseModel): """ diff --git a/antarest/study/storage/explorer_service.py b/antarest/study/storage/explorer_service.py index fa9ef7fa30..5610f3e5f8 100644 --- a/antarest/study/storage/explorer_service.py +++ b/antarest/study/storage/explorer_service.py @@ -14,7 +14,7 @@ from typing import List from antarest.core.config import Config -from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolderDTO, WorkspaceMetadata +from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolder, WorkspaceMetadata from antarest.study.storage.utils import ( get_folder_from_workspace, get_workspace_from_config, @@ -33,7 +33,7 @@ def list_dir( self, workspace_name: str, workspace_directory_path: str, - ) -> List[NonStudyFolderDTO]: + ) -> List[NonStudyFolder]: """ return a list of all directories under workspace_directory_path, that aren't studies. """ @@ -44,7 +44,7 @@ def list_dir( if child.is_dir() and not is_study_folder(child) and not should_ignore_folder_for_scan(child): # we don't want to expose the full absolute path on the server child_rel_path = child.relative_to(workspace.path) - directories.append(NonStudyFolderDTO(path=child_rel_path, workspace=workspace_name, name=child.name)) + directories.append(NonStudyFolder(path=child_rel_path, workspace=workspace_name, name=child.name)) return directories def list_workspaces( diff --git a/antarest/study/web/explorer_blueprint.py b/antarest/study/web/explorer_blueprint.py index b453cab787..0981ba5214 100644 --- a/antarest/study/web/explorer_blueprint.py +++ b/antarest/study/web/explorer_blueprint.py @@ -18,7 +18,7 @@ from antarest.core.config import Config from antarest.core.jwt import JWTUser from antarest.login.auth import Auth -from antarest.study.model import NonStudyFolderDTO, WorkspaceMetadata +from antarest.study.model import NonStudyFolder, WorkspaceMetadata from antarest.study.storage.explorer_service import Explorer logger = logging.getLogger(__name__) @@ -40,13 +40,13 @@ def create_explorer_routes(config: Config, explorer: Explorer) -> APIRouter: @bp.get( "/explorer/{workspace}/_list_dir", summary="For a given directory, list sub directories that aren't studies", - response_model=List[NonStudyFolderDTO], + response_model=List[NonStudyFolder], ) def list_dir( workspace: str, path: str, current_user: JWTUser = Depends(auth.get_current_user), - ) -> List[NonStudyFolderDTO]: + ) -> List[NonStudyFolder]: """ Endpoint to list sub directories of a given directory Args: diff --git a/tests/integration/explorer_blueprint/test_explorer.py b/tests/integration/explorer_blueprint/test_explorer.py index 31990b1781..dbb6f83ebc 100644 --- a/tests/integration/explorer_blueprint/test_explorer.py +++ b/tests/integration/explorer_blueprint/test_explorer.py @@ -14,7 +14,7 @@ import pytest from starlette.testclient import TestClient -from antarest.study.model import NonStudyFolderDTO, WorkspaceMetadata +from antarest.study.model import NonStudyFolder, WorkspaceMetadata BAD_REQUEST_STATUS_CODE = 400 # Status code for directory listing with invalid parameters @@ -65,9 +65,9 @@ def test_explorer(client: TestClient, admin_access_token: str, study_tree: Path) ) res.raise_for_status() directories_res = res.json() - directories_res = [NonStudyFolderDTO(**d) for d in directories_res] + directories_res = [NonStudyFolder(**d) for d in directories_res] directorires_expected = [ - NonStudyFolderDTO( + NonStudyFolder( path=Path("folder/trash"), workspace="ext", name="trash", diff --git a/tests/storage/business/test_explorer_service.py b/tests/storage/business/test_explorer_service.py index 37a7c0c033..883e79cfca 100644 --- a/tests/storage/business/test_explorer_service.py +++ b/tests/storage/business/test_explorer_service.py @@ -15,7 +15,7 @@ import pytest from antarest.core.config import Config, StorageConfig, WorkspaceConfig -from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolderDTO, WorkspaceMetadata +from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolder, WorkspaceMetadata from antarest.study.storage.explorer_service import Explorer @@ -85,7 +85,8 @@ def test_list_dir_empty_string(config_scenario_a: Config): result = explorer.list_dir("diese", "") assert len(result) == 1 - assert result[0] == NonStudyFolderDTO(path=Path("folder"), workspace="diese", name="folder") + workspace_path = config_scenario_a.get_workspace_path(workspace="diese") + assert result[0] == NonStudyFolder(path=Path("folder"), workspace="diese", name="folder") @pytest.mark.unit_test @@ -94,10 +95,11 @@ def test_list_dir_several_subfolders(config_scenario_a: Config): result = explorer.list_dir("diese", "folder") assert len(result) == 3 + workspace_path = config_scenario_a.get_workspace_path(workspace="diese") folder_path = Path("folder") - assert NonStudyFolderDTO(path=(folder_path / "subfolder1"), workspace="diese", name="subfolder1") in result - assert NonStudyFolderDTO(path=(folder_path / "subfolder2"), workspace="diese", name="subfolder2") in result - assert NonStudyFolderDTO(path=(folder_path / "subfolder3"), workspace="diese", name="subfolder3") in result + assert NonStudyFolder(path=(folder_path / "subfolder1"), workspace="diese", name="subfolder1") in result + assert NonStudyFolder(path=(folder_path / "subfolder2"), workspace="diese", name="subfolder2") in result + assert NonStudyFolder(path=(folder_path / "subfolder3"), workspace="diese", name="subfolder3") in result @pytest.mark.unit_test diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 1f13b6e551..250a75f2e6 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -642,9 +642,7 @@ "studies.studylaunched": "{{studyname}} launched!", "studies.copySuffix": "Copy", "studies.filters.strictfolder": "Show only direct folder children", - "studies.filters.showAllDescendants": "Show all children", "studies.scanFolder": "Scan folder", - "studies.requestDeepScan": "Recursive scan", "studies.moveStudy": "Move", "studies.movefolderplaceholder": "Path separated by '/'", "studies.importcopy": "Copy to database", @@ -676,9 +674,6 @@ "studies.exportOutputFilter": "Export filtered output", "studies.selectOutput": "Select an output", "studies.variant": "Variant", - "studies.tree.error.failToFetchWorkspace": "Failed to load workspaces", - "studies.tree.error.failToFetchFolder": "Failed to load subfolders for {{path}}", - "studies.tree.error.detailsInConsole": "Details logged in the console", "variants.createNewVariant": "Create new variant", "variants.newVariant": "New variant", "variants.newCommand": "Add new command", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 14dbcda923..98d6ee71b9 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -642,9 +642,7 @@ "studies.studylaunched": "{{studyname}} lancé(s) !", "studies.copySuffix": "Copie", "studies.filters.strictfolder": "Afficher uniquement les descendants directs", - "studies.filters.showAllDescendants": "Voir les sous-dossiers", "studies.scanFolder": "Scanner le dossier", - "studies.requestDeepScan": "Scan récursif", "studies.moveStudy": "Déplacer", "studies.movefolderplaceholder": "Chemin séparé par des '/'", "studies.importcopy": "Copier en base", @@ -675,9 +673,7 @@ "studies.exportOutput": "Exporter une sortie", "studies.exportOutputFilter": "Exporter une sortie filtrée", "studies.selectOutput": "Selectionnez une sortie", - "studies.tree.error.failToFetchWorkspace": "Échec lors de la récupération de l'espace de travail", - "studies.tree.error.failToFetchFolder": "Échec lors de la récupération des sous dossiers de {{path}}", - "studies.tree.error.detailsInConsole": "Détails de l'érreur dans la console", + "studies.variant": "Variante", "variants.createNewVariant": "Créer une nouvelle variante", "variants.newVariant": "Nouvelle variante", "variants.newCommand": "Ajouter une nouvelle commande", diff --git a/webapp/src/components/App/Studies/SideNav.tsx b/webapp/src/components/App/Studies/SideNav.tsx index b966f27fd6..c0009ce7d2 100644 --- a/webapp/src/components/App/Studies/SideNav.tsx +++ b/webapp/src/components/App/Studies/SideNav.tsx @@ -16,7 +16,7 @@ import { useNavigate } from "react-router"; import { Box, Typography, List, ListItem, ListItemText } from "@mui/material"; import { useTranslation } from "react-i18next"; import { STUDIES_SIDE_NAV_WIDTH } from "../../../theme"; -import StudyTree from "@/components/App/Studies/StudyTree"; +import StudyTree from "./StudyTree"; import useAppSelector from "../../../redux/hooks/useAppSelector"; import { getFavoriteStudies } from "../../../redux/selectors"; diff --git a/webapp/src/components/App/Studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx index b8b32f1646..6d90785471 100644 --- a/webapp/src/components/App/Studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -33,8 +33,7 @@ import AutoSizer from "react-virtualized-auto-sizer"; import HomeIcon from "@mui/icons-material/Home"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; -import FolderIcon from "@mui/icons-material/Folder"; -import AccountTreeIcon from "@mui/icons-material/AccountTree"; +import FolderOffIcon from "@mui/icons-material/FolderOff"; import RadarIcon from "@mui/icons-material/Radar"; import { FixedSizeGrid, GridOnScrollProps } from "react-window"; import { v4 as uuidv4 } from "uuid"; @@ -62,7 +61,6 @@ import RefreshButton from "../RefreshButton"; import { scanFolder } from "../../../../services/api/study"; import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; -import CheckBoxFE from "@/components/common/fieldEditors/CheckBoxFE"; const CARD_TARGET_WIDTH = 500; const CARD_HEIGHT = 250; @@ -90,7 +88,6 @@ function StudiesList(props: StudiesListProps) { const [selectedStudies, setSelectedStudies] = useState([]); const [selectionMode, setSelectionMode] = useState(false); const [confirmFolderScan, setConfirmFolderScan] = useState(false); - const [isRecursiveScan, setIsRecursiveScan] = useState(false); useEffect(() => { setFolderList(folder.split("/")); @@ -159,18 +156,13 @@ function StudiesList(props: StudiesListProps) { try { // Remove "/root" from the path const folder = folderList.slice(1).join("/"); - await scanFolder(folder, isRecursiveScan); + await scanFolder(folder); setConfirmFolderScan(false); - setIsRecursiveScan(false); } catch (e) { enqueueErrorSnackbar(t("studies.error.scanFolder"), e as AxiosError); } }; - const handleRecursiveScan = () => { - setIsRecursiveScan(!isRecursiveScan); - }; - //////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////// @@ -257,21 +249,13 @@ function StudiesList(props: StudiesListProps) { ({`${studyIds.length} ${t("global.studies").toLowerCase()}`}) - - {strictFolderFilter ? ( - - - - - - ) : ( - - - - - - )} - + + + + + {folder !== "root" && ( setConfirmFolderScan(true)}> @@ -282,20 +266,12 @@ function StudiesList(props: StudiesListProps) { {folder !== "root" && confirmFolderScan && ( { - setConfirmFolderScan(false); - setIsRecursiveScan(false); - }} + onCancel={() => setConfirmFolderScan(false)} onConfirm={handleFolderScan} alert="warning" open > {`${t("studies.scanFolder")} ${folder}?`} - )} diff --git a/webapp/src/components/App/Studies/StudyTree.tsx b/webapp/src/components/App/Studies/StudyTree.tsx new file mode 100644 index 0000000000..7208caaec4 --- /dev/null +++ b/webapp/src/components/App/Studies/StudyTree.tsx @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2024, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import { StudyTreeNode } from "./utils"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getStudiesTree, getStudyFilters } from "../../../redux/selectors"; +import useAppDispatch from "../../../redux/hooks/useAppDispatch"; +import { updateStudyFilters } from "../../../redux/ducks/studies"; +import TreeItemEnhanced from "../../common/TreeItemEnhanced"; +import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView"; +import { getParentPaths } from "../../../utils/pathUtils"; +import * as R from "ramda"; + +function StudyTree() { + const folder = useAppSelector((state) => getStudyFilters(state).folder, R.T); + const studiesTree = useAppSelector(getStudiesTree); + const dispatch = useAppDispatch(); + + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + const handleTreeItemClick = (itemId: string) => { + dispatch(updateStudyFilters({ folder: itemId })); + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + const buildTree = (children: StudyTreeNode[], parentId?: string) => { + return children.map((child) => { + const id = parentId ? `${parentId}/${child.name}` : child.name; + + return ( + handleTreeItemClick(id)} + > + {buildTree(child.children, id)} + + ); + }); + }; + + return ( + + {buildTree([studiesTree])} + + ); +} + +export default StudyTree; diff --git a/webapp/src/components/App/Studies/StudyTree/__test__/fixtures.ts b/webapp/src/components/App/Studies/StudyTree/__test__/fixtures.ts deleted file mode 100644 index a6a4dbc3ad..0000000000 --- a/webapp/src/components/App/Studies/StudyTree/__test__/fixtures.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) 2024, RTE (https://www.rte-france.com) - * - * See AUTHORS.txt - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - * - * This file is part of the Antares project. - */ - -export const FIXTURES = { - basicTree: { - name: "Basic tree with single level", - studyTree: { - name: "Root", - path: "/", - children: [ - { name: "a", path: "/a", children: [] }, - { name: "b", path: "/b", children: [] }, - ], - }, - folders: [ - { - name: "folder1", - path: "folder1", - workspace: "a", - parentPath: "/a", - }, - ], - expected: { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "folder1", path: "/a/folder1", children: [] }], - }, - { name: "b", path: "/b", children: [] }, - ], - }, - }, - nestedTree: { - name: "Nested tree structure", - studyTree: { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "suba", path: "/a/suba", children: [] }], - }, - ], - }, - folders: [ - { - name: "folder1", - path: "suba/folder1", - workspace: "a", - parentPath: "/a/suba", - }, - ], - expected: { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [ - { - name: "suba", - path: "/a/suba", - children: [ - { name: "folder1", path: "/a/suba/folder1", children: [] }, - ], - }, - ], - }, - ], - }, - }, - duplicateCase: { - name: "Tree with potential duplicates", - studyTree: { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "folder1", path: "/a/folder1", children: [] }], - }, - ], - }, - folders: [ - { - name: "folder1", - path: "/folder1", - workspace: "a", - parentPath: "/a", - }, - ], - expected: { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "folder1", path: "/a/folder1", children: [] }], - }, - ], - }, - }, - multipleFolders: { - name: "Multiple folders merge", - studyTree: { - name: "Root", - path: "/", - children: [{ name: "a", path: "/a", children: [] }], - }, - folders: [ - { - name: "folder1", - path: "/folder1", - workspace: "a", - parentPath: "/a", - }, - { - name: "folder2", - path: "/folder2", - workspace: "a", - parentPath: "/a", - }, - ], - expected: { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [ - { name: "folder1", path: "/a/folder1", children: [] }, - { name: "folder2", path: "/a/folder2", children: [] }, - ], - }, - ], - }, - }, -}; diff --git a/webapp/src/components/App/Studies/StudyTree/__test__/utils.test.tsx b/webapp/src/components/App/Studies/StudyTree/__test__/utils.test.tsx deleted file mode 100644 index 87a5419c54..0000000000 --- a/webapp/src/components/App/Studies/StudyTree/__test__/utils.test.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2024, RTE (https://www.rte-france.com) - * - * See AUTHORS.txt - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - * - * This file is part of the Antares project. - */ - -import { insertFoldersIfNotExist, insertWorkspacesIfNotExist } from "../utils"; -import { NonStudyFolderDTO, StudyTreeNode } from "../../utils"; -import { FIXTURES } from "./fixtures"; - -describe("StudyTree Utils", () => { - describe("mergeStudyTreeAndFolders", () => { - test.each(Object.values(FIXTURES))( - "$name", - ({ studyTree, folders, expected }) => { - const result = insertFoldersIfNotExist(studyTree, folders); - expect(result).toEqual(expected); - }, - ); - - test("should handle empty study tree", () => { - const emptyTree: StudyTreeNode = { - name: "Root", - path: "/", - children: [], - }; - const result = insertFoldersIfNotExist(emptyTree, []); - expect(result).toEqual(emptyTree); - }); - - test("should handle empty folders array", () => { - const tree: StudyTreeNode = { - name: "Root", - path: "/", - children: [{ name: "a", path: "/a", children: [] }], - }; - const result = insertFoldersIfNotExist(tree, []); - expect(result).toEqual(tree); - }); - - test("should handle invalid parent paths", () => { - const tree: StudyTreeNode = { - name: "Root", - path: "/", - children: [{ name: "a", path: "/a", children: [] }], - }; - const invalidFolder: NonStudyFolderDTO = { - name: "invalid", - path: "/invalid", - workspace: "nonexistent", - parentPath: "/nonexistent", - }; - const result = insertFoldersIfNotExist(tree, [invalidFolder]); - expect(result).toEqual(tree); - }); - - test("should handle empty workspaces", () => { - const tree: StudyTreeNode = { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "suba", path: "/a/suba", children: [] }], - }, - ], - }; - const workspaces: string[] = []; - const result = insertWorkspacesIfNotExist(tree, workspaces); - expect(result).toEqual(tree); - }); - - test("should merge workspaces", () => { - const tree: StudyTreeNode = { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "suba", path: "/a/suba", children: [] }], - }, - ], - }; - const expected: StudyTreeNode = { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "suba", path: "/a/suba", children: [] }], - }, - { name: "workspace1", path: "/workspace1", children: [] }, - { name: "workspace2", path: "/workspace2", children: [] }, - ], - }; - - const workspaces: string[] = ["a", "workspace1", "workspace2"]; - const result = insertWorkspacesIfNotExist(tree, workspaces); - expect(result).toEqual(expected); - }); - }); -}); diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx deleted file mode 100644 index 510d70134e..0000000000 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Copyright (c) 2024, RTE (https://www.rte-france.com) - * - * See AUTHORS.txt - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - * - * This file is part of the Antares project. - */ - -import { StudyTreeNode } from ".././utils"; -import useAppSelector from "../../../../redux/hooks/useAppSelector"; -import { getStudiesTree, getStudyFilters } from "../../../../redux/selectors"; -import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; -import { updateStudyFilters } from "../../../../redux/ducks/studies"; -import TreeItemEnhanced from "../../../common/TreeItemEnhanced"; -import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView"; -import { getParentPaths } from "../../../../utils/pathUtils"; -import * as R from "ramda"; -import { useState } from "react"; -import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; -import useUpdateEffectOnce from "@/hooks/useUpdateEffectOnce"; -import { fetchAndInsertSubfolders, fetchAndInsertWorkspaces } from "./utils"; -import { useTranslation } from "react-i18next"; -import { toError } from "@/utils/fnUtils"; - -function StudyTree() { - const initialStudiesTree = useAppSelector(getStudiesTree); - const [studiesTree, setStudiesTree] = useState(initialStudiesTree); - const folder = useAppSelector((state) => getStudyFilters(state).folder, R.T); - const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); - const dispatch = useAppDispatch(); - const [t] = useTranslation(); - - // Initialize folders once we have the tree - // we use useUpdateEffectOnce because at first render initialStudiesTree isn't initialized - useUpdateEffectOnce(() => { - updateTree("root", initialStudiesTree); - }, [initialStudiesTree]); - - /** - * This function is called at the initialization of the component and when the user clicks on a folder. - * - * The study tree is built from the studies in the database. There's a scan process that run on the server - * to update continuously the studies in the database. - * - * However this process can take a long time, and the user shouldn't wait for hours before he can see a study he knows is already uploaded. - * - * Instead of relying on the scan process to update the tree, we'll allow the user to walk into the tree and run a scan process only when he needs to. - * - * To enable this, we'll fetch the subfolders of a folder when the user clicks on it using the explorer API. - * - * @param itemId - The id of the item clicked - * @param studyTreeNode - The node of the item clicked - */ - async function updateTree(itemId: string, studyTreeNode: StudyTreeNode) { - let treeAfterWorkspacesUpdate = studiesTree; - let chidrenPaths = studyTreeNode.children.map( - (child) => `root${child.path}`, - ); - // If the user clicks on the root folder, we fetch the workspaces and insert them. - // Then we fetch the direct subfolders of the workspaces. - if (itemId === "root") { - try { - treeAfterWorkspacesUpdate = await fetchAndInsertWorkspaces(studiesTree); - chidrenPaths = treeAfterWorkspacesUpdate.children.map( - (child) => `root${child.path}`, - ); - } catch (error) { - enqueueErrorSnackbar( - "studies.tree.error.failToFetchWorkspace", - toError(error), - ); - } - } else { - // If the user clicks on a folder, we add the path of the clicked folder to the list of paths to fetch. - // as well as the path of the children of the clicked folder. - // If we don't fetch the subfolders of the children then we won't know if they're themselves folders, which we need - // to know to display the little arrow next to the subfolder. - // On the other hand, if we fetch only the subfolders of the children, then we won't fetch their "siblings" folder - // if one of them is added. - chidrenPaths = [studyTreeNode.path].concat(chidrenPaths); - } - - const [treeAfterChildrenUpdate, failedPath] = - await fetchAndInsertSubfolders(chidrenPaths, treeAfterWorkspacesUpdate); - if (failedPath.length > 0) { - enqueueErrorSnackbar( - t("studies.tree.error.failToFetchFolder", { - path: failedPath.join(" "), - interpolation: { escapeValue: false }, - }), - t("studies.tree.error.detailsInConsole"), - ); - } - setStudiesTree(treeAfterChildrenUpdate); - } - - //////////////////////////////////////////////////////////////// - // Event Handlers - //////////////////////////////////////////////////////////////// - - const handleTreeItemClick = async ( - itemId: string, - studyTreeNode: StudyTreeNode, - ) => { - dispatch(updateStudyFilters({ folder: itemId })); - updateTree(itemId, studyTreeNode); - }; - - //////////////////////////////////////////////////////////////// - // JSX - //////////////////////////////////////////////////////////////// - - const buildTree = (children: StudyTreeNode[], parentId?: string) => { - return children.map((child) => { - const id = parentId ? `${parentId}/${child.name}` : child.name; - - return ( - handleTreeItemClick(id, child)} - > - {buildTree(child.children, id)} - - ); - }); - }; - - return ( - - {buildTree([studiesTree])} - - ); -} - -export default StudyTree; diff --git a/webapp/src/components/App/Studies/StudyTree/utils.ts b/webapp/src/components/App/Studies/StudyTree/utils.ts deleted file mode 100644 index 14c4a5b008..0000000000 --- a/webapp/src/components/App/Studies/StudyTree/utils.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Copyright (c) 2024, RTE (https://www.rte-france.com) - * - * See AUTHORS.txt - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - * - * This file is part of the Antares project. - */ - -import { NonStudyFolderDTO, StudyTreeNode } from "../utils"; -import * as api from "../../../../services/api/study"; - -/** - * Add a folder that was returned by the explorer into the study tree view. - * - * This function doesn't mutate the tree, it returns a new tree with the folder inserted. - * - * If the folder is already in the tree, the tree returnred will be equal to the tree given to the function. - * - * @param studiesTree study tree to insert the folder into - * @param folder folder to inert into the tree - * @returns study tree with the folder inserted if it wasn't already there. - * New branch is created if it contain the folder otherwise the branch is left unchanged. - */ -function insertFolderIfNotExist( - studiesTree: StudyTreeNode, - folder: NonStudyFolderDTO, -): StudyTreeNode { - // Early return if folder doesn't belong in this branch - if (!folder.parentPath.startsWith(studiesTree.path)) { - return studiesTree; - } - - // direct child case - if (folder.parentPath == studiesTree.path) { - const folderExists = studiesTree.children.some( - (child) => child.name === folder.name, - ); - if (folderExists) { - return studiesTree; - } - // parent path is the same, but no folder with the same name at this level - return { - ...studiesTree, - children: [ - ...studiesTree.children, - { - path: `${folder.parentPath}/${folder.name}`, - name: folder.name, - children: [], - }, - ], - }; - } - - // not a direct child, but does belong to this branch so recursively walk though the tree - return { - ...studiesTree, - children: studiesTree.children.map((child) => - insertFolderIfNotExist(child, folder), - ), - }; -} - -/** - * Insert several folders in the study tree if they don't exist already in the tree. - * - * This function doesn't mutate the tree, it returns a new tree with the folders inserted - * - * The folders are inserted in the order they are given. - * - * @param studiesTree study tree to insert the folder into - * @param folders folders to inert into the tree - * @param studiesTree study tree to insert the folder into - * @param folder folder to inert into the tree - * @returns study tree with the folder inserted if it wasn't already there. - * New branch is created if it contain the folder otherwise the branch is left unchanged. - */ -export function insertFoldersIfNotExist( - studiesTree: StudyTreeNode, - folders: NonStudyFolderDTO[], -): StudyTreeNode { - return folders.reduce( - (tree, folder) => insertFolderIfNotExist(tree, folder), - studiesTree, - ); -} - -/** - * Call the explorer api to fetch the subfolders under the given path. - * - * @param path path of the subfolder to fetch, should sart with root, e.g. root/workspace/folder1 - * @returns list of subfolders under the given path - */ -async function fetchSubfolders(path: string): Promise { - if (path === "root") { - // Under root there're workspaces not subfolders - return []; - } - // less than 2 parts means we're at the root level - const pathParts = path.split("/"); - if (pathParts.length < 2) { - return []; - } - // path parts should be ["root", workspace, "folder1", ...] - const workspace = pathParts[1]; - const subPath = pathParts.slice(2).join("/"); - return api.getFolders(workspace, subPath); -} - -/** - * Fetch and insert the subfolders under the given paths into the study tree. - * - * This function is used to fill the study tree when the user clicks on a folder. - * - * Subfolders are inserted only if they don't exist already in the tree. - * - * This function doesn't mutate the tree, it returns a new tree with the subfolders inserted - * - * @param paths list of paths to fetch the subfolders for - * @param studiesTree study tree to insert the subfolders into - * @returns a tuple with study tree with the subfolders inserted if they weren't already there and path for which - * the fetch failed. - */ -export async function fetchAndInsertSubfolders( - paths: string[], - studiesTree: StudyTreeNode, -): Promise<[StudyTreeNode, string[]]> { - const results = await Promise.allSettled( - paths.map((path) => fetchSubfolders(path)), - ); - - return results.reduce<[StudyTreeNode, string[]]>( - ([tree, failed], result, index) => { - if (result.status === "fulfilled") { - return [insertFoldersIfNotExist(tree, result.value), failed]; - } - console.error("Failed to load path:", paths[index], result.reason); - return [tree, [...failed, paths[index]]]; - }, - [studiesTree, []], - ); -} - -/** - * Insert a workspace into the study tree if it doesn't exist already. - * - * This function doesn't mutate the tree, it returns a new tree with the workspace inserted. - * - * @param workspace key of the workspace - * @param stydyTree study tree to insert the workspace into - * @returns study tree with the empty workspace inserted if it wasn't already there. - */ -function insertWorkspaceIfNotExist( - stydyTree: StudyTreeNode, - workspace: string, -) { - const emptyNode = { name: workspace, path: `/${workspace}`, children: [] }; - if (stydyTree.children.some((child) => child.name === workspace)) { - return stydyTree; - } - return { - ...stydyTree, - children: [...stydyTree.children, emptyNode], - }; -} - -/** - * Insert several workspaces into the study tree if they don't exist already in the tree. - * - * This function doesn't mutate the tree, it returns a new tree with the workspaces inserted. - * - * The workspaces are inserted in the order they are given. - * - * @param workspaces workspaces to insert into the tree - * @param stydyTree study tree to insert the workspaces into - * @returns study tree with the empty workspaces inserted if they weren't already there. - */ -export function insertWorkspacesIfNotExist( - stydyTree: StudyTreeNode, - workspaces: string[], -): StudyTreeNode { - return workspaces.reduce((acc, workspace) => { - return insertWorkspaceIfNotExist(acc, workspace); - }, stydyTree); -} - -/** - * Fetch and insert the workspaces into the study tree. - * - * Workspaces are inserted only if they don't exist already in the tree. - * - * This function doesn't mutate the tree, it returns a new tree with the workspaces inserted. - * - * @param studyTree study tree to insert the workspaces into - * @returns study tree with the workspaces inserted if they weren't already there. - */ -export async function fetchAndInsertWorkspaces( - studyTree: StudyTreeNode, -): Promise { - const workspaces = await api.getWorkspaces(); - return insertWorkspacesIfNotExist(studyTree, workspaces); -} diff --git a/webapp/src/components/App/Studies/utils.ts b/webapp/src/components/App/Studies/utils.ts index 8117450702..3f2ff61564 100644 --- a/webapp/src/components/App/Studies/utils.ts +++ b/webapp/src/components/App/Studies/utils.ts @@ -20,13 +20,6 @@ export interface StudyTreeNode { children: StudyTreeNode[]; } -export interface NonStudyFolderDTO { - name: string; - path: string; - workspace: string; - parentPath: string; -} - /** * Builds a tree structure from a list of study metadata. * diff --git a/webapp/src/redux/ducks/studies.ts b/webapp/src/redux/ducks/studies.ts index 9b4603a23f..3b3e8b04d2 100644 --- a/webapp/src/redux/ducks/studies.ts +++ b/webapp/src/redux/ducks/studies.ts @@ -94,7 +94,7 @@ const initialState = studiesAdapter.getInitialState({ filters: { inputValue: "", folder: "root", - strictFolder: true, + strictFolder: false, managed: false, archived: false, variant: false, diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 349134d790..53d4a67925 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -34,11 +34,6 @@ import { getConfig } from "../config"; import { convertStudyDtoToMetadata } from "../utils"; import { FileDownloadTask } from "./downloads"; import { StudyMapDistrict } from "../../redux/ducks/studyMaps"; -import { NonStudyFolderDTO } from "@/components/App/Studies/utils"; - -interface Workspace { - name: string; -} const getStudiesRaw = async (): Promise> => { const res = await client.get(`/v1/studies`); @@ -53,30 +48,6 @@ export const getStudies = async (): Promise => { }); }; -export const getWorkspaces = async (): Promise => { - const res = await client.get( - `/v1/private/explorer/_list_workspaces`, - ); - return res.data.map((folder) => folder.name); -}; - -/** - * Call the explorer API to get the list of folders in a workspace - * - * @param workspace - workspace name - * @param folderPath - path starting from the workspace root (not including the workspace name) - * @returns list of folders that are not studies, under the given path - */ -export const getFolders = async ( - workspace: string, - folderPath: string, -): Promise => { - const res = await client.get( - `/v1/private/explorer/${workspace}/_list_dir?path=${encodeURIComponent(folderPath)}`, - ); - return res.data; -}; - export const getStudyVersions = async (): Promise => { const res = await client.get("/v1/studies/_versions"); return res.data; @@ -463,13 +434,8 @@ export const updateStudyMetadata = async ( return res.data; }; -export const scanFolder = async ( - folderPath: string, - recursive = false, -): Promise => { - await client.post( - `/v1/watcher/_scan?path=${encodeURIComponent(folderPath)}&recursive=${recursive}`, - ); +export const scanFolder = async (folderPath: string): Promise => { + await client.post(`/v1/watcher/_scan?path=${encodeURIComponent(folderPath)}`); }; export const getStudyLayers = async (uuid: string): Promise => {