Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/615 optimize listing studies - Bugs strikes back #2288

Open
wants to merge 58 commits into
base: dev
Choose a base branch
from

Conversation

smailio
Copy link
Contributor

@smailio smailio commented Jan 9, 2025

No description provided.

Anis SMAIL added 30 commits December 4, 2024 13:48
…cursive scan, show only studies in the current folder by default

use a tree icon when we show studies for all descendants
Comment on lines +645 to +647
"studies.filters.showAllDescendants": "Show all children",
"studies.scanFolder": "Scan folder",
"studies.requestDeepScan": "Recursive scan",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest simplifying the i18n keys to match their UI labels more directly:

studies.filters.showAllDescendantsstudies.filters.showChildrens
studies.requestDeepScanstudies.recursiveScan
(in the code you dont see the UI label so it can be a bit confusing when you search a specific key that correspond to the UI text)

@@ -674,6 +676,10 @@
"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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For toast notification text, we should focus on user-friendly messages rather than technical details ("Details logged in the console"), I suggest you remove this one as you already raise an appropriate message for the error

@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip: TypeScript can infer the type from useState's default value, so you can simplify:
useState<boolean>(false)useState(false). Exception to this rule when initializing with a different type than what the state will eventually hold:

// Starting empty but hold numbers
const [items, setItems] = useState<number[]>([]);

Comment on lines 19 to 20
id: "xxxxxxxxxx",
name: "XXXXXXXXXX",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be something meaningful like:
id: "test-study-id",
name: "Test Study"

Comment on lines +253 to +256
mkStudyMetadata("plop1/plop2/xxx1", "workspace"),
mkStudyMetadata("plop1/plop3/xxx2", "workspace"),
mkStudyMetadata("plop1/plop4/xxx3", "workspace"),
mkStudyMetadata("plouf1/plouf2/xxx4", "workspace2"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please avoid using these kind of non-descriptive names, instead prefer names that have business meaning like:
"studies/projects/project1"
"studies/archived/study1"
"workspace1/active/study2"

@@ -48,6 +53,30 @@ export const getStudies = async (): Promise<StudyMetadata[]> => {
});
};

export const getWorkspaces = async (): Promise<string[]> => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return type not necessary here

export const getFolders = async (
workspace: string,
folderPath: string,
): Promise<NonStudyFolderDTO[]> => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can omit the return type here

export const scanFolder = async (
folderPath: string,
recursive = false,
): Promise<void> => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can omit the return type here

// JSX
////////////////////////////////////////////////////////////////

const buildTree = (children: StudyTreeNode[], parentId?: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildTree is recreated on each render of StudyTree, which is potentially expensive for large tree structures with many children, also to keep the components cleaner it's better to refactor it into a dedicated module, here's what i suggest:

  1. Extract the tree node rendering logic into a separate memoized component
  2. Memoize the click handlers to prevent unnecessary re-renders (you can check if its really necessary)
  3. Leverage component composition instead of using a recursive function

Suggested implementation:

const StudyTreeNode = memo(function StudyTreeNode({ node, parentId, onNodeClick }: StudyTreeNodeProps) {
  const id = parentId ? `${parentId}/${node.name}` : node.name;
  
  const handleClick = useCallback(() => {
    onNodeClick(id, node);
  }, [id, node, onNodeClick]);

  const isLoading = node.hasChildren && node.children.length === 0;

  return (
    <TreeItemEnhanced 
      itemId={id} 
      label={node.name} 
      onClick={handleClick}
    >
      {isLoading ? (
        <TreeItemEnhanced
          itemId={id + "loading"}
          label={t("studies.tree.fetchFolderLoading")}
        />
      ) : (
        node.children.map((child) => (
          <StudyTreeNode
            key={`${id}/${child.name}`}
            node={child}
            parentId={id}
            onNodeClick={onNodeClick}
          />
        ))
      )}
    </TreeItemEnhanced>
  );
});
// In StudyTree component:
function StudyTree() {
  const handleNodeClick = useCallback((itemId: string, node: StudyTreeNode) => {
    dispatch(updateStudyFilters({ folder: itemId }));
    updateTree(itemId, studiesTree, node);
  }, [dispatch, studiesTree]);

  return (
    <SimpleTreeView
      defaultExpandedItems={[...getParentPaths(folder), folder]}
      defaultSelectedItems={folder}
      sx={{ ... }}
    >
      <StudyTreeNode node={studiesTree} onNodeClick={handleNodeClick} />
    </SimpleTreeView>
  );
}

child.hasChildren && child.children.length === 0;
const id = parentId ? `${parentId}/${child.name}` : child.name;

if (shouldDisplayLoading) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing wrong about this but generally prefer using prefix is, like "isDoingSomething" for flags (the common React boolean flag naming convention), here isLoadingFolder or isLoading seems better

@@ -332,6 +332,22 @@ class NonStudyFolder(AntaresBaseModel):
path: Path
workspace: str
name: str
has_children: bool = Field(
alias="hasChildren",
) # true when has non study folder children (false when has no children or only study children)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really clear. You could simplify by writing # true when has at least one non-study-folder children

# 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))
has_children = has_non_study_folder(child)
directories.append(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor:
using the camel case to instantiate the class isn't perfect. You can try to use a dict and do NonStudyFolderDTO.model_validate(...) with the dict keys being has_children, it could work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants