[ARVADOS-WORKBENCH2] updated: 1.4.1-369-ge92207c9

Git user git at public.arvados.org
Tue Jun 23 14:40:17 UTC 2020


Summary of changes:
 package.json                                       |   4 +
 src/components/file-tree/file-tree.tsx             |   3 +-
 src/components/tree/tree.tsx                       |   1 +
 src/components/tree/virtual-tree.tsx               | 251 +++++++++++++++++++++
 src/models/tree.ts                                 |   2 +
 .../collection-panel-files-actions.ts              |  10 +
 .../collection-panel-files.ts                      |  44 +++-
 yarn.lock                                          |  32 +++
 8 files changed, 344 insertions(+), 3 deletions(-)
 create mode 100644 src/components/tree/virtual-tree.tsx

       via  e92207c912aed73a07340b5fb2a9e2cb23e1da5f (commit)
      from  7131c732cffb1383099b5e1593b3ee4b48635df5 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit e92207c912aed73a07340b5fb2a9e2cb23e1da5f
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Jun 23 11:37:54 2020 -0300

    15610: Uses a virtualized list to show the collection's file tree. (WIP)
    
    This greatly improves rendering times when showing collections with many
    files on a directory.
    This is a POC: the whole tree is rendered expanded to show that it doesn't
    affect render times, it still needs lots of tweaking to offer the same
    behavior as before.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/package.json b/package.json
index 0efdbd7d..57c6e311 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
     "@types/react-copy-to-clipboard": "4.2.6",
     "@types/react-dropzone": "4.2.2",
     "@types/react-highlight-words": "0.12.0",
+    "@types/react-virtualized-auto-sizer": "1.0.0",
+    "@types/react-window": "1.8.2",
     "@types/redux-form": "7.4.12",
     "@types/shell-quote": "1.6.0",
     "axios": "0.18.1",
@@ -53,6 +55,8 @@
     "react-scripts-ts": "3.1.0",
     "react-splitter-layout": "3.0.1",
     "react-transition-group": "2.5.0",
+    "react-virtualized-auto-sizer": "1.0.2",
+    "react-window": "1.8.5",
     "redux": "4.0.3",
     "redux-form": "7.4.2",
     "redux-thunk": "2.3.0",
diff --git a/src/components/file-tree/file-tree.tsx b/src/components/file-tree/file-tree.tsx
index 34a11cd6..b5d98c08 100644
--- a/src/components/file-tree/file-tree.tsx
+++ b/src/components/file-tree/file-tree.tsx
@@ -3,7 +3,8 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { Tree, TreeItem, TreeItemStatus } from "../tree/tree";
+import { TreeItem, TreeItemStatus } from "../tree/tree";
+import { VirtualTree as Tree } from "../tree/virtual-tree";
 import { FileTreeData } from "./file-tree-data";
 import { FileTreeItem } from "./file-tree-item";
 
diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx
index 76fbf011..b5ce5ec5 100644
--- a/src/components/tree/tree.tsx
+++ b/src/components/tree/tree.tsx
@@ -79,6 +79,7 @@ export interface TreeItem<T> {
     selected?: boolean;
     status: TreeItemStatus;
     items?: Array<TreeItem<T>>;
+    level?: number;
 }
 
 export interface TreeProps<T> {
diff --git a/src/components/tree/virtual-tree.tsx b/src/components/tree/virtual-tree.tsx
new file mode 100644
index 00000000..4615db4f
--- /dev/null
+++ b/src/components/tree/virtual-tree.tsx
@@ -0,0 +1,251 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
+import { ReactElement } from "react";
+import { FixedSizeList, ListChildComponentProps } from "react-window";
+import AutoSizer from "react-virtualized-auto-sizer";
+// import {FixedSizeTree as Tree} from 'react-vtree';
+
+import { ArvadosTheme } from '~/common/custom-theme';
+import { TreeItem } from './tree';
+// import { FileTreeData } from '../file-tree/file-tree-data';
+
+type CssRules = 'list'
+    | 'listItem'
+    | 'active'
+    | 'loader'
+    | 'toggableIconContainer'
+    | 'iconClose'
+    | 'renderContainer'
+    | 'iconOpen'
+    | 'toggableIcon'
+    | 'checkbox'
+    | 'virtualizedList';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    list: {
+        padding: '3px 0px',
+    },
+    virtualizedList: {
+        height: '200px',
+    },
+    listItem: {
+        padding: '3px 0px',
+    },
+    loader: {
+        position: 'absolute',
+        transform: 'translate(0px)',
+        top: '3px'
+    },
+    toggableIconContainer: {
+        color: theme.palette.grey["700"],
+        height: '14px',
+        width: '14px',
+    },
+    toggableIcon: {
+        fontSize: '14px'
+    },
+    renderContainer: {
+        flex: 1
+    },
+    active: {
+        color: theme.palette.primary.main,
+    },
+    iconClose: {
+        transition: 'all 0.1s ease',
+    },
+    iconOpen: {
+        transition: 'all 0.1s ease',
+        transform: 'rotate(90deg)',
+    },
+    checkbox: {
+        width: theme.spacing.unit * 3,
+        height: theme.spacing.unit * 3,
+        margin: `0 ${theme.spacing.unit}px`,
+        padding: 0,
+        color: theme.palette.grey["500"],
+    }
+});
+
+export interface TreeProps<T> {
+    disableRipple?: boolean;
+    currentItemUuid?: string;
+    items?: Array<TreeItem<T>>;
+    level?: number;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+    render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
+    showSelection?: boolean | ((item: TreeItem<T>) => boolean);
+    levelIndentation?: number;
+    itemRightPadding?: number;
+    toggleItemActive: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+    toggleItemOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+    toggleItemSelection?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+
+    /**
+     * When set to true use radio buttons instead of checkboxes for item selection.
+     * This does not guarantee radio group behavior (i.e item mutual exclusivity).
+     * Any item selection logic must be done in the toggleItemActive callback prop.
+     */
+    useRadioButtons?: boolean;
+}
+
+// export const RowA = <T, _>(items: TreeItem<T>[], render:any) => (index: number) => {
+//     return <div>
+//         {render(items[index])}
+//     </div>;
+// };
+
+// For some reason, on TSX files it isn't accepted just one generic param, so
+// I'm using <T, _> as a workaround.
+export const Row = <T, _>(items: TreeItem<T>[], render: any) => (props: React.PropsWithChildren<ListChildComponentProps>) => {
+    const { index, style } = props;
+    const level = items[index].level || 0;
+    const levelIndentation = 20;
+    return <div style={style}>
+        <div style={{ paddingLeft: (level + 1) * levelIndentation,}}>
+            {typeof render === 'function'
+                ? items[index] && render(items[index]) || ''
+                : 'whoops'}
+        </div>
+    </div>;
+    // <div style={style} key={`item/${level}/${idx}`}>
+    //     <ListItem button className={listItem}
+    //         style={{
+    //             paddingLeft: (level + 1) * levelIndentation,
+    //             paddingRight: itemRightPadding,
+    //         }}
+    //         disableRipple={disableRipple}
+    //         onClick={event => toggleItemActive(event, it)}
+    //         selected={showSelection(it) && it.id === currentItemUuid}
+    //         onContextMenu={this.handleRowContextMenu(it)}>
+    //         {it.status === TreeItemStatus.PENDING ?
+    //             <CircularProgress size={10} className={loader} /> : null}
+    //         <i onClick={this.handleToggleItemOpen(it)}
+    //             className={toggableIconContainer}>
+    //             <ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
+    //                 {this.getProperArrowAnimation(it.status, it.items!)}
+    //             </ListItemIcon>
+    //         </i>
+    //         {showSelection(it) && !useRadioButtons &&
+    //             <Checkbox
+    //                 checked={it.selected}
+    //                 className={classes.checkbox}
+    //                 color="primary"
+    //                 onClick={this.handleCheckboxChange(it)} />}
+    //         {showSelection(it) && useRadioButtons &&
+    //             <Radio
+    //                 checked={it.selected}
+    //                 className={classes.checkbox}
+    //                 color="primary" />}
+    //         <div className={renderContainer}>
+    //             {render(it, level)}
+    //         </div>
+    //     </ListItem>
+    //     {it.items && it.items.length > 0 &&
+    //         <Collapse in={it.open} timeout="auto" unmountOnExit>
+    //             <Tree
+    //                 showSelection={this.props.showSelection}
+    //                 items={it.items}
+    //                 render={render}
+    //                 disableRipple={disableRipple}
+    //                 toggleItemOpen={toggleItemOpen}
+    //                 toggleItemActive={toggleItemActive}
+    //                 level={level + 1}
+    //                 onContextMenu={onContextMenu}
+    //                 toggleItemSelection={this.props.toggleItemSelection} />
+    //         </Collapse>}
+    // </div>
+};
+
+export const VirtualList = <T, _>(height: number, width: number, items: TreeItem<T>[], render: any) =>
+    <FixedSizeList
+        height={height}
+        itemCount={items.length}
+        itemSize={30}
+        width={width}
+    >
+        {Row(items, render)}
+    </FixedSizeList>;
+
+export const VirtualTree = withStyles(styles)(
+    class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
+        render(): ReactElement<any> {
+            const { items, render } = this.props;
+
+            return <div className={this.props.classes.virtualizedList}><AutoSizer>
+                {({ height, width }) => {
+                    return VirtualList(height, width, items || [], render);
+                }}
+            </AutoSizer></div>;
+        }
+    }
+);
+
+// const treeWalkerWithTree = (tree: Array<TreeItem<FileTreeData>>) => function* treeWalker(refresh: any) {
+//     const stack = [];
+
+//     // Remember all the necessary data of the first node in the stack.
+//     stack.push({
+//       nestingLevel: 0,
+//       node: tree,
+//     });
+
+//     // Walk through the tree until we have no nodes available.
+//     while (stack.length !== 0) {
+//         const {
+//             node: {items = [], id, name},
+//             nestingLevel,
+//         } = stack.pop()!;
+
+//         // Here we are sending the information about the node to the Tree component
+//         // and receive an information about the openness state from it. The
+//         // `refresh` parameter tells us if the full update of the tree is requested;
+//         // basing on it we decide to return the full node data or only the node
+//         // id to update the nodes order.
+//         const isOpened = yield refresh
+//             ? {
+//                 id,
+//                 isLeaf: items.length === 0,
+//                 isOpenByDefault: true,
+//                 name,
+//                 nestingLevel,
+//             }
+//             : id;
+
+//         // Basing on the node openness state we are deciding if we need to render
+//         // the child nodes (if they exist).
+//         if (children.length !== 0 && isOpened) {
+//             // Since it is a stack structure, we need to put nodes we want to render
+//             // first to the end of the stack.
+//             for (let i = children.length - 1; i >= 0; i--) {
+//                 stack.push({
+//                     nestingLevel: nestingLevel + 1,
+//                     node: children[i],
+//                 });
+//             }
+//         }
+//     }
+// };
+
+// // Node component receives all the data we created in the `treeWalker` +
+// // internal openness state (`isOpen`), function to change internal openness
+// // state (`toggle`) and `style` parameter that should be added to the root div.
+// const Node = ({data: {isLeaf, name}, isOpen, style, toggle}) => (
+//     <div style={style}>
+//         {!isLeaf && (
+//         <button type="button" onClick={toggle}>
+//             {isOpen ? '-' : '+'}
+//         </button>
+//         )}
+//         <div>{name}</div>
+//     </div>
+// );
+
+// export const Example = () => (
+//     <Tree treeWalker={treeWalker} itemSize={30} height={150} width={300}>
+//         {Node}
+//     </Tree>
+// );
\ No newline at end of file
diff --git a/src/models/tree.ts b/src/models/tree.ts
index c7713cbc..69224059 100644
--- a/src/models/tree.ts
+++ b/src/models/tree.ts
@@ -16,6 +16,7 @@ export interface TreeNode<T = any> {
     selected: boolean;
     expanded: boolean;
     status: TreeNodeStatus;
+    level?: number;
 }
 
 export enum TreeNodeStatus {
@@ -193,6 +194,7 @@ export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & { pare
     expanded: false,
     status: TreeNodeStatus.INITIAL,
     parent: '',
+    level: 0,
     ...data,
 });
 
diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
index 204d4c0e..175a8cef 100644
--- a/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
+++ b/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
@@ -31,15 +31,25 @@ export const COLLECTION_PANEL_LOAD_FILES_THRESHOLD = 40000;
 
 export const loadCollectionFiles = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        let step = Date.now();
         dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PANEL_LOAD_FILES));
         const files = await services.collectionService.files(uuid);
+        console.log('Get files: ', (Date.now()-step)/1000);
 
         // Given the array of directories and files, create the appropriate tree nodes,
         // sort them, and add the complete url to each.
+        step = Date.now();
         const tree = createCollectionFilesTree(files);
+        console.log('Create tree: ', (Date.now()-step)/1000);
+        step = Date.now();
         const sorted = sortFilesTree(tree);
+        console.log('Sort tree: ', (Date.now()-step)/1000);
+        step = Date.now();
         const mapped = mapTreeValues(services.collectionService.extendFileURL)(sorted);
+        console.log('Add URL: ', (Date.now()-step)/1000);
+        step = Date.now();
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(mapped));
+        console.log('Dispatch: ', (Date.now()-step)/1000);
         dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PANEL_LOAD_FILES));
     };
 
diff --git a/src/views-components/collection-panel-files/collection-panel-files.ts b/src/views-components/collection-panel-files/collection-panel-files.ts
index eb16eb6c..e0798086 100644
--- a/src/views-components/collection-panel-files/collection-panel-files.ts
+++ b/src/views-components/collection-panel-files/collection-panel-files.ts
@@ -32,8 +32,12 @@ const memoizedMapStateToProps = () => {
     return (state: RootState): Pick<CollectionPanelFilesProps, "items" | "currentItemUuid"> => {
         if (prevState !== state.collectionPanelFiles) {
             prevState = state.collectionPanelFiles;
-            prevTree = getNodeChildrenIds('')(state.collectionPanelFiles)
-                .map(collectionItemToTreeItem(state.collectionPanelFiles));
+            prevTree = [].concat.apply(
+                [], getNodeChildrenIds('')(state.collectionPanelFiles)
+                    .map(collectionItemToList(0)(state.collectionPanelFiles)))
+                .map(nodeToTreeItem);
+            // prevTree = getNodeChildrenIds('')(state.collectionPanelFiles)
+            //     .map(collectionItemToTreeItem(state.collectionPanelFiles));
         }
         return {
             items: prevTree,
@@ -77,6 +81,24 @@ const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps,
 
 export const CollectionPanelFiles = connect(memoizedMapStateToProps(), mapDispatchToProps)(Component);
 
+export const collectionItemToList = (level: number) => (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
+    (id: string): TreeItem<FileTreeData>[] => {
+        const node: TreeNode<CollectionPanelDirectory | CollectionPanelFile> = getNode(id)(tree) || initTreeNode({
+            id: '',
+            parent: '',
+            value: {
+                ...createCollectionDirectory({ name: 'Invalid file' }),
+                selected: false,
+                collapsed: true
+            }
+        });
+        const childs = [].concat.apply([], node.children.map(collectionItemToList(level+1)(tree)));
+        return [
+            {...node, level},
+            ...childs,
+        ];
+    };
+
 const collectionItemToTreeItem = (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
     (id: string): TreeItem<FileTreeData> => {
         const node: TreeNode<CollectionPanelDirectory | CollectionPanelFile> = getNode(id)(tree) || initTreeNode({
@@ -104,3 +126,21 @@ const collectionItemToTreeItem = (tree: Tree<CollectionPanelDirectory | Collecti
             status: TreeItemStatus.LOADED
         };
     };
+
+const nodeToTreeItem = (node: TreeNode<CollectionPanelDirectory | CollectionPanelFile>): TreeItem<FileTreeData> => {
+    return ({
+        active: false,
+        data: {
+            name: node.value.name,
+            size: node.value.type === CollectionFileType.FILE ? node.value.size : undefined,
+            type: node.value.type,
+            url: node.value.url,
+        },
+        id: node.id,
+        items: [],
+        open: node.value.type === CollectionFileType.DIRECTORY ? !node.value.collapsed : false,
+        selected: node.value.selected,
+        status: TreeItemStatus.LOADED,
+        level: node.level,
+    });
+};
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index d6677a74..742db465 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -427,6 +427,20 @@
   dependencies:
     "@types/react" "*"
 
+"@types/react-virtualized-auto-sizer at 1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.0.tgz#fc32f30a8dab527b5816f3a757e1e1d040c8f272"
+  integrity sha512-NMErdIdSnm2j/7IqMteRiRvRulpjoELnXWUwdbucYCz84xG9PHcoOrr7QfXwB/ku7wd6egiKFrzt/+QK4Imeeg==
+  dependencies:
+    "@types/react" "*"
+
+"@types/react-window at 1.8.2":
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.2.tgz#a5a6b2762ce73ffaab7911ee1397cf645f2459fe"
+  integrity sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react@*":
   version "16.9.11"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.11.tgz#70e0b7ad79058a7842f25ccf2999807076ada120"
@@ -7063,6 +7077,11 @@ mem@^4.0.0:
     mimic-fn "^2.0.0"
     p-is-promise "^2.0.0"
 
+"memoize-one@>=3.1.1 <6":
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
+  integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
+
 memoize-one@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906"
@@ -9081,6 +9100,19 @@ react-transition-group@^2.2.1:
     prop-types "^15.6.2"
     react-lifecycles-compat "^3.0.4"
 
+react-virtualized-auto-sizer at 1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
+  integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==
+
+react-window at 1.8.5:
+  version "1.8.5"
+  resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1"
+  integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==
+  dependencies:
+    "@babel/runtime" "^7.0.0"
+    memoize-one ">=3.1.1 <6"
+
 react at 16.8.6:
   version "16.8.6"
   resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list