Skip to content

Commit

Permalink
feat(ui-studies): add on click fetch and display list of non studies …
Browse files Browse the repository at this point in the history
…folder (#2224)

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.

The scan button now by default run a non recursive scan, that way it takes less time. 
 
When the user clicks on a folder we fetch its subfolders using the explorer API. This enable the user to navigate to subfolders that aren't discovered by the scan yet, so subfolders that wouldn't be visible before hours if we rely only on the scan. 

By combining these two features, the user won't need to wait the full scan to complete, instead , he'll walk into the tree and run a fast scan process only subfolder he needs. 

This commit is mostly a front commit but also make small adjustment on the back.
  • Loading branch information
smailio authored Dec 18, 2024
1 parent 4e70a35 commit 763f370
Show file tree
Hide file tree
Showing 17 changed files with 750 additions and 110 deletions.
17 changes: 15 additions & 2 deletions antarest/study/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from pathlib import Path

from antares.study.version import StudyVersion
from pydantic import BeforeValidator, PlainSerializer, field_validator
from pydantic import BeforeValidator, PlainSerializer, computed_field, field_validator
from sqlalchemy import ( # type: ignore
Boolean,
Column,
Expand Down Expand Up @@ -323,7 +323,7 @@ class StudyFolder:
groups: t.List[Group]


class NonStudyFolder(AntaresBaseModel):
class NonStudyFolderDTO(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
Expand All @@ -333,6 +333,19 @@ class NonStudyFolder(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):
"""
Expand Down
6 changes: 3 additions & 3 deletions antarest/study/storage/explorer_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from typing import List

from antarest.core.config import Config
from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolder, WorkspaceMetadata
from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolderDTO, WorkspaceMetadata
from antarest.study.storage.utils import (
get_folder_from_workspace,
get_workspace_from_config,
Expand All @@ -33,7 +33,7 @@ def list_dir(
self,
workspace_name: str,
workspace_directory_path: str,
) -> List[NonStudyFolder]:
) -> List[NonStudyFolderDTO]:
"""
return a list of all directories under workspace_directory_path, that aren't studies.
"""
Expand All @@ -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(NonStudyFolder(path=child_rel_path, workspace=workspace_name, name=child.name))
directories.append(NonStudyFolderDTO(path=child_rel_path, workspace=workspace_name, name=child.name))
return directories

def list_workspaces(
Expand Down
6 changes: 3 additions & 3 deletions antarest/study/web/explorer_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 NonStudyFolder, WorkspaceMetadata
from antarest.study.model import NonStudyFolderDTO, WorkspaceMetadata
from antarest.study.storage.explorer_service import Explorer

logger = logging.getLogger(__name__)
Expand All @@ -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[NonStudyFolder],
response_model=List[NonStudyFolderDTO],
)
def list_dir(
workspace: str,
path: str,
current_user: JWTUser = Depends(auth.get_current_user),
) -> List[NonStudyFolder]:
) -> List[NonStudyFolderDTO]:
"""
Endpoint to list sub directories of a given directory
Args:
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/explorer_blueprint/test_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import pytest
from starlette.testclient import TestClient

from antarest.study.model import NonStudyFolder, WorkspaceMetadata
from antarest.study.model import NonStudyFolderDTO, WorkspaceMetadata

BAD_REQUEST_STATUS_CODE = 400
# Status code for directory listing with invalid parameters
Expand Down Expand Up @@ -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 = [NonStudyFolder(**d) for d in directories_res]
directories_res = [NonStudyFolderDTO(**d) for d in directories_res]
directorires_expected = [
NonStudyFolder(
NonStudyFolderDTO(
path=Path("folder/trash"),
workspace="ext",
name="trash",
Expand Down
12 changes: 5 additions & 7 deletions tests/storage/business/test_explorer_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import pytest

from antarest.core.config import Config, StorageConfig, WorkspaceConfig
from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolder, WorkspaceMetadata
from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolderDTO, WorkspaceMetadata
from antarest.study.storage.explorer_service import Explorer


Expand Down Expand Up @@ -85,8 +85,7 @@ def test_list_dir_empty_string(config_scenario_a: Config):
result = explorer.list_dir("diese", "")

assert len(result) == 1
workspace_path = config_scenario_a.get_workspace_path(workspace="diese")
assert result[0] == NonStudyFolder(path=Path("folder"), workspace="diese", name="folder")
assert result[0] == NonStudyFolderDTO(path=Path("folder"), workspace="diese", name="folder")


@pytest.mark.unit_test
Expand All @@ -95,11 +94,10 @@ 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 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
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


@pytest.mark.unit_test
Expand Down
5 changes: 5 additions & 0 deletions webapp/public/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,9 @@
"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",
Expand Down Expand Up @@ -674,6 +676,9 @@
"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",
Expand Down
6 changes: 5 additions & 1 deletion webapp/public/locales/fr/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,9 @@
"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",
Expand Down Expand Up @@ -673,7 +675,9 @@
"studies.exportOutput": "Exporter une sortie",
"studies.exportOutputFilter": "Exporter une sortie filtrée",
"studies.selectOutput": "Selectionnez une sortie",
"studies.variant": "Variante",
"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",
"variants.createNewVariant": "Créer une nouvelle variante",
"variants.newVariant": "Nouvelle variante",
"variants.newCommand": "Ajouter une nouvelle commande",
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/components/App/Studies/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 "./StudyTree";
import StudyTree from "@/components/App/Studies/StudyTree";
import useAppSelector from "../../../redux/hooks/useAppSelector";
import { getFavoriteStudies } from "../../../redux/selectors";

Expand Down
44 changes: 34 additions & 10 deletions webapp/src/components/App/Studies/StudiesList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ 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 FolderOffIcon from "@mui/icons-material/FolderOff";
import FolderIcon from "@mui/icons-material/Folder";
import AccountTreeIcon from "@mui/icons-material/AccountTree";
import RadarIcon from "@mui/icons-material/Radar";
import { FixedSizeGrid, GridOnScrollProps } from "react-window";
import { v4 as uuidv4 } from "uuid";
Expand Down Expand Up @@ -61,6 +62,7 @@ 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;
Expand Down Expand Up @@ -88,6 +90,7 @@ function StudiesList(props: StudiesListProps) {
const [selectedStudies, setSelectedStudies] = useState<string[]>([]);
const [selectionMode, setSelectionMode] = useState(false);
const [confirmFolderScan, setConfirmFolderScan] = useState<boolean>(false);
const [isRecursiveScan, setIsRecursiveScan] = useState<boolean>(false);

useEffect(() => {
setFolderList(folder.split("/"));
Expand Down Expand Up @@ -156,13 +159,18 @@ function StudiesList(props: StudiesListProps) {
try {
// Remove "/root" from the path
const folder = folderList.slice(1).join("/");
await scanFolder(folder);
await scanFolder(folder, isRecursiveScan);
setConfirmFolderScan(false);
setIsRecursiveScan(false);
} catch (e) {
enqueueErrorSnackbar(t("studies.error.scanFolder"), e as AxiosError);
}
};

const handleRecursiveScan = () => {
setIsRecursiveScan(!isRecursiveScan);
};

////////////////////////////////////////////////////////////////
// Utils
////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -249,13 +257,21 @@ function StudiesList(props: StudiesListProps) {
<Typography mx={2} sx={{ color: "white" }}>
({`${studyIds.length} ${t("global.studies").toLowerCase()}`})
</Typography>
<Tooltip title={t("studies.filters.strictfolder") as string}>
<IconButton onClick={toggleStrictFolder}>
<FolderOffIcon
color={strictFolderFilter ? "secondary" : "disabled"}
/>
</IconButton>
</Tooltip>

{strictFolderFilter ? (
<Tooltip title={t("studies.filters.strictfolder")}>
<IconButton onClick={toggleStrictFolder}>
<FolderIcon color="secondary" />
</IconButton>
</Tooltip>
) : (
<Tooltip title={t("studies.filters.showAllDescendants")}>
<IconButton onClick={toggleStrictFolder}>
<AccountTreeIcon color="secondary" />
</IconButton>
</Tooltip>
)}

{folder !== "root" && (
<Tooltip title={t("studies.scanFolder") as string}>
<IconButton onClick={() => setConfirmFolderScan(true)}>
Expand All @@ -266,12 +282,20 @@ function StudiesList(props: StudiesListProps) {
{folder !== "root" && confirmFolderScan && (
<ConfirmationDialog
titleIcon={RadarIcon}
onCancel={() => setConfirmFolderScan(false)}
onCancel={() => {
setConfirmFolderScan(false);
setIsRecursiveScan(false);
}}
onConfirm={handleFolderScan}
alert="warning"
open
>
{`${t("studies.scanFolder")} ${folder}?`}
<CheckBoxFE
label={t("studies.requestDeepScan")}
value={isRecursiveScan}
onChange={handleRecursiveScan}
/>
</ConfirmationDialog>
)}
</Box>
Expand Down
77 changes: 0 additions & 77 deletions webapp/src/components/App/Studies/StudyTree.tsx

This file was deleted.

Loading

0 comments on commit 763f370

Please sign in to comment.