-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: dev
Are you sure you want to change the base?
Conversation
…factor code into a functional style
…cursive scan, show only studies in the current folder by default use a tree icon when we show studies for all descendants
"studies.filters.showAllDescendants": "Show all children", | ||
"studies.scanFolder": "Scan folder", | ||
"studies.requestDeepScan": "Recursive scan", |
There was a problem hiding this comment.
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.showAllDescendants
→ studies.filters.showChildrens
studies.requestDeepScan
→ studies.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", |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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[]>([]);
id: "xxxxxxxxxx", | ||
name: "XXXXXXXXXX", |
There was a problem hiding this comment.
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"
mkStudyMetadata("plop1/plop2/xxx1", "workspace"), | ||
mkStudyMetadata("plop1/plop3/xxx2", "workspace"), | ||
mkStudyMetadata("plop1/plop4/xxx3", "workspace"), | ||
mkStudyMetadata("plouf1/plouf2/xxx4", "workspace2"), |
There was a problem hiding this comment.
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[]> => { |
There was a problem hiding this comment.
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[]> => { |
There was a problem hiding this comment.
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> => { |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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:
- Extract the tree node rendering logic into a separate memoized component
- Memoize the click handlers to prevent unnecessary re-renders (you can check if its really necessary)
- 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) { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
No description provided.