Merge branch '20424-io-panel-performance' into main. Closes #20420
authorStephen Smith <stephen@curii.com>
Wed, 10 May 2023 20:57:12 +0000 (16:57 -0400)
committerStephen Smith <stephen@curii.com>
Wed, 10 May 2023 20:57:12 +0000 (16:57 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

cypress/integration/project.spec.js
package.json
src/components/collection-panel-files/collection-panel-files.tsx
src/routes/route-change-handlers.ts
src/services/groups-service/groups-service.ts
src/store/project-panel/project-panel-action.ts
src/store/store.ts
src/store/workbench/workbench-actions.ts
yarn.lock

index 68a90133d25270f937bfdb6d4ecb299036902040..eff4d4e9f18651fc322b7374b0d4b03adf613fbb 100644 (file)
@@ -135,6 +135,7 @@ describe('Project tests', function() {
                 });
             });
         cy.get('[data-cy=form-submit-btn]').click();
+        cy.get('[data-cy=form-dialog]').should('not.exist');
 
         const editProjectDescription = (name, type) => {
             cy.get('[data-cy=side-panel-tree]').contains('Home Projects').click();
@@ -545,7 +546,10 @@ describe('Project tests', function() {
                 cy.get('[data-cy=form-submit-btn]').click();
             });
         cy.get('[data-cy=form-dialog]').should("not.exist");
+        cy.get('[data-cy=snackbar]').contains('created');
+        cy.get('[data-cy=snackbar]').should("not.exist");
         cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+        cy.waitForDom();
         cy.get('[data-cy=project-panel]').contains(projectName).should('be.visible').rightclick();
         cy.get('[data-cy=context-menu]').contains('Copy to clipboard').click();
         cy.window().then((win) => (
index c17fc9174500db0ad7cbe43788d9c18d4c1574e4..0a10da786e5bc1621a0ea84721ffddaf8a634042 100644 (file)
@@ -68,6 +68,7 @@
     "react-virtualized-auto-sizer": "1.0.2",
     "react-window": "1.8.5",
     "redux": "4.0.3",
+    "redux-devtools-extension": "^2.13.9",
     "redux-form": "7.4.2",
     "redux-thunk": "2.3.0",
     "reselect": "4.0.0",
index fb36ebce549d25171e38fed562db654d887a79ed..ddf30364ad9c76f09f006d33035d407255d20f7b 100644 (file)
@@ -39,6 +39,7 @@ import { setCollectionFiles } from 'store/collection-panel/collection-panel-file
 import { sortBy } from 'lodash';
 import { formatFileSize } from 'common/formatters';
 import { getInlineFileUrl, sanitizeToken } from 'views-components/context-menu/actions/helpers';
+import { extractUuidKind, ResourceKind } from 'models/resource';
 
 export interface CollectionPanelFilesProps {
     isWritable: boolean;
@@ -260,7 +261,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
     const rightData = pathData[rightKey];
 
     React.useEffect(() => {
-        if (props.currentItemUuid) {
+        if (props.currentItemUuid && extractUuidKind(props.currentItemUuid) === ResourceKind.COLLECTION) {
             setPathData({});
             setPath([props.currentItemUuid]);
         }
@@ -288,33 +289,33 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
             })
             .filter((promise) => !!promise)
         )
-        .then((requests) => {
-            const newState = requests.map((request, index) => {
-                if (request && request.responseXML != null) {
-                    const key = keyArray[index];
-                    const result: any = extractFilesData(request.responseXML);
-                    const sortedResult = sortBy(result, (n) => n.name).sort((n1, n2) => {
-                        if (n1.type === 'directory' && n2.type !== 'directory') {
-                            return -1;
-                        }
-                        if (n1.type !== 'directory' && n2.type === 'directory') {
-                            return 1;
-                        }
-                        return 0;
-                    });
-
-                    return { [key]: sortedResult };
-                }
-                return {};
-            }).reduce((prev, next) => {
-                return { ...next, ...prev };
-            }, {});
-            setPathData((state) => ({ ...state, ...newState }));
-        })
-        .finally(() => {
-            setIsLoading(false);
-            keyArray.forEach(key => delete pathPromise[key]);
-        });
+            .then((requests) => {
+                const newState = requests.map((request, index) => {
+                    if (request && request.responseXML != null) {
+                        const key = keyArray[index];
+                        const result: any = extractFilesData(request.responseXML);
+                        const sortedResult = sortBy(result, (n) => n.name).sort((n1, n2) => {
+                            if (n1.type === 'directory' && n2.type !== 'directory') {
+                                return -1;
+                            }
+                            if (n1.type !== 'directory' && n2.type === 'directory') {
+                                return 1;
+                            }
+                            return 0;
+                        });
+
+                        return { [key]: sortedResult };
+                    }
+                    return {};
+                }).reduce((prev, next) => {
+                    return { ...next, ...prev };
+                }, {});
+                setPathData((state) => ({ ...state, ...newState }));
+            })
+            .finally(() => {
+                setIsLoading(false);
+                keyArray.forEach(key => delete pathPromise[key]);
+            });
     };
 
     React.useEffect(() => {
@@ -323,7 +324,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
             setLeftSearch('');
             setRightSearch('');
         }
-    }, [rightKey]); // eslint-disable-line react-hooks/exhaustive-deps
+    }, [rightKey, rightData]); // eslint-disable-line react-hooks/exhaustive-deps
 
     const currentPDH = (collectionPanel.item || {}).portableDataHash;
     React.useEffect(() => {
@@ -420,7 +421,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                 }
 
                 if (elem.dataset.id && type === 'file') {
-                    const item = rightData.find(({id}) => id === elem.dataset.id) || leftData.find(({ id }) => id === elem.dataset.id);
+                    const item = rightData.find(({ id }) => id === elem.dataset.id) || leftData.find(({ id }) => id === elem.dataset.id);
                     const enhancedItem = servicesProvider.getServices().collectionService.extendFileURL(item);
                     const fileUrl = sanitizeToken(getInlineFileUrl(enhancedItem.url, config.keepWebServiceUrl, config.keepWebInlineServiceUrl), true);
                     window.open(fileUrl, '_blank');
@@ -483,12 +484,12 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
     return <div data-cy="collection-files-panel" onClick={handleClick} ref={parentRef}>
         <div className={classes.pathPanel}>
             <div className={classes.pathPanelPathWrapper}>
-            { path.map( (p: string, index: number) =>
-                <span key={`${index}-${p}`} data-item="true"
-                className={classes.pathPanelItem} data-breadcrumb-path={p}>
-                    <span className={classes.rowActive}>{index === 0 ? 'Home' : p}</span> <b>/</b>&nbsp;
-                </span>)
-            }
+                {path.map((p: string, index: number) =>
+                    <span key={`${index}-${p}`} data-item="true"
+                        className={classes.pathPanelItem} data-breadcrumb-path={p}>
+                        <span className={classes.rowActive}>{index === 0 ? 'Home' : p}</span> <b>/</b>&nbsp;
+                    </span>)
+                }
             </div>
             <Tooltip className={classes.pathPanelMenu} title="More options" disableFocusListener>
                 <IconButton data-cy='collection-files-panel-options-btn'
@@ -502,94 +503,96 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
         <div className={classes.wrapper}>
             <div className={classNames(classes.leftPanel, path.length > 1 ? classes.leftPanelVisible : classes.leftPanelHidden)} data-cy="collection-files-left-panel">
                 <Tooltip title="Go back" className={path.length > 1 ? classes.backButton : classes.backButtonHidden}>
-                    <IconButton onClick={() => setPath((state) => ([...state.slice(0, state.length -1)]))}>
+                    <IconButton onClick={() => setPath((state) => ([...state.slice(0, state.length - 1)]))}>
                         <BackIcon />
                     </IconButton>
                 </Tooltip>
                 <div className={path.length > 1 ? classes.searchWrapper : classes.searchWrapperHidden}>
                     <SearchInput selfClearProp={leftKey} label="Search" value={leftSearch} onSearch={setLeftSearch} />
                 </div>
-                <div className={classes.dataWrapper}>{ leftData
-                ? <AutoSizer defaultWidth={0}>{({ height, width }) => {
-                    const filtered = leftData.filter(({ name }) => name.indexOf(leftSearch) > -1);
-                    return !!filtered.length
-                    ? <FixedSizeList height={height} itemCount={filtered.length}
-                        itemSize={35} width={width}>{ ({ index, style }) => {
-                        const { id, type, name } = filtered[index];
-                        return <div data-id={id} style={style} data-item="true"
-                            data-type={type} data-parent-path={name}
-                            className={classNames(classes.row, getActiveClass(name))}
-                            key={id}>
-                                { getItemIcon(type, getActiveClass(name)) }
-                                <div className={classes.rowName}>
-                                    {name}
-                                </div>
-                                getActiveClass(name)
-                                ? <SidePanelRightArrowIcon
-                                    style={{ display: 'inline', marginTop: '5px', marginLeft: '5px' }} />
-                                : null
-                                }
-                        </div>;
-                    }}</FixedSizeList>
-                    : <div className={classes.rowEmpty}>No directories available</div>
+                <div className={classes.dataWrapper}>{leftData
+                    ? <AutoSizer defaultWidth={0}>{({ height, width }) => {
+                        const filtered = leftData.filter(({ name }) => name.indexOf(leftSearch) > -1);
+                        return !!filtered.length
+                            ? <FixedSizeList height={height} itemCount={filtered.length}
+                                itemSize={35} width={width}>{({ index, style }) => {
+                                    const { id, type, name } = filtered[index];
+                                    return <div data-id={id} style={style} data-item="true"
+                                        data-type={type} data-parent-path={name}
+                                        className={classNames(classes.row, getActiveClass(name))}
+                                        key={id}>
+                                        {getItemIcon(type, getActiveClass(name))}
+                                        <div className={classes.rowName}>
+                                            {name}
+                                        </div>
+                                        {getActiveClass(name)
+                                            ? <SidePanelRightArrowIcon
+                                                style={{ display: 'inline', marginTop: '5px', marginLeft: '5px' }} />
+                                            : null
+                                        }
+                                    </div>;
+                                }}</FixedSizeList>
+                            : <div className={classes.rowEmpty}>No directories available</div>
                     }}
-                </AutoSizer>
-                : <div data-cy="collection-loader" className={classes.row}><CircularProgress className={classes.loader} size={30} /></div> }
+                    </AutoSizer>
+                    : <div data-cy="collection-loader" className={classes.row}><CircularProgress className={classes.loader} size={30} /></div>}
                 </div>
             </div>
             <div className={classes.rightPanel} data-cy="collection-files-right-panel">
                 <div className={classes.searchWrapper}>
                     <SearchInput selfClearProp={rightKey} label="Search" value={rightSearch} onSearch={setRightSearch} />
                 </div>
-                { isWritable &&
-                <Button className={classes.uploadButton} data-cy='upload-button'
-                    onClick={() => {
-                        onUploadDataClick(rightKey === leftKey ? undefined : rightKey);
-                    }}
-                    variant='contained' color='primary' size='small'>
-                    <DownloadIcon className={classes.uploadIcon} />
-                    Upload data
-                </Button> }
-                <div className={classes.dataWrapper}>{ rightData && !isLoading
+                {isWritable &&
+                    <Button className={classes.uploadButton} data-cy='upload-button'
+                        onClick={() => {
+                            onUploadDataClick(rightKey === leftKey ? undefined : rightKey);
+                        }}
+                        variant='contained' color='primary' size='small'>
+                        <DownloadIcon className={classes.uploadIcon} />
+                        Upload data
+                    </Button>}
+                <div className={classes.dataWrapper}>{rightData && !isLoading
                     ? <AutoSizer defaultHeight={500}>{({ height, width }) => {
                         const filtered = rightData.filter(({ name }) => name.indexOf(rightSearch) > -1);
                         return !!filtered.length
-                        ? <FixedSizeList height={height} itemCount={filtered.length}
-                            itemSize={35} width={width}>{ ({ index, style }) => {
-                                const { id, type, name, size } = filtered[index];
-
-                                return <div style={style} data-id={id} data-item="true"
-                                    data-type={type} data-subfolder-path={name}
-                                    className={classes.row} key={id}>
-                                    <Checkbox color="primary"
-                                        className={classes.rowSelection}
-                                        checked={collectionPanelFiles[id] ? collectionPanelFiles[id].value.selected : false}
-                                    />&nbsp;
-                                    {getItemIcon(type, null)}
-                                    <div className={classes.rowName}>
-                                        {name}
+                            ? <FixedSizeList height={height} itemCount={filtered.length}
+                                itemSize={35} width={width}>{({ index, style }) => {
+                                    const { id, type, name, size } = filtered[index];
+
+                                    return <div style={style} data-id={id} data-item="true"
+                                        data-type={type} data-subfolder-path={name}
+                                        className={classes.row} key={id}>
+                                        <Checkbox color="primary"
+                                            className={classes.rowSelection}
+                                            checked={collectionPanelFiles[id] ? collectionPanelFiles[id].value.selected : false}
+                                        />&nbsp;
+                                        {getItemIcon(type, null)}
+                                        <div className={classes.rowName}>
+                                            {name}
+                                        </div>
+                                        <span className={classes.rowName} style={{
+                                            marginLeft: 'auto', marginRight: '1rem'
+                                        }}>
+                                            {formatFileSize(size)}
+                                        </span>
+                                        <Tooltip title="More options" disableFocusListener>
+                                            <IconButton data-id='moreOptions'
+                                                data-cy='file-item-options-btn'
+                                                className={classes.moreOptionsButton}>
+                                                <MoreOptionsIcon
+                                                    data-id='moreOptions'
+                                                    className={classes.moreOptions} />
+                                            </IconButton>
+                                        </Tooltip>
                                     </div>
-                                    <span className={classes.rowName} style={{
-                                        marginLeft: 'auto', marginRight: '1rem' }}>
-                                        { formatFileSize(size) }
-                                    </span>
-                                    <Tooltip title="More options" disableFocusListener>
-                                        <IconButton data-id='moreOptions'
-                                            data-cy='file-item-options-btn'
-                                            className={classes.moreOptionsButton}>
-                                            <MoreOptionsIcon
-                                                data-id='moreOptions'
-                                                className={classes.moreOptions} />
-                                        </IconButton>
-                                    </Tooltip>
-                                </div>
-                            } }</FixedSizeList>
-                        : <div className={classes.rowEmpty}>This collection is empty</div>
+                                }}</FixedSizeList>
+                            : <div className={classes.rowEmpty}>This collection is empty</div>
                     }}</AutoSizer>
                     : <div className={classes.row}>
                         <CircularProgress className={classes.loader} size={30} />
-                    </div> }
+                    </div>}
                 </div>
             </div>
         </div>
-    </div>}));
+    </div>
+}));
index cded6d65cd38dcce7e00fac722131399c324feec..eef52058529b69acd74f17416552561382487e03 100644 (file)
@@ -11,6 +11,7 @@ import { dialogActions } from 'store/dialog/dialog-actions';
 import { contextMenuActions } from 'store/context-menu/context-menu-actions';
 import { searchBarActions } from 'store/search-bar/search-bar-actions';
 import { pluginConfig } from 'plugins';
+import { openProjectPanel } from 'store/project-panel/project-panel-action';
 
 export const addRouteChangeHandlers = (history: History, store: RootStore) => {
     const handler = handleLocationChange(store);
@@ -60,7 +61,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     }
 
     if (projectMatch) {
-        store.dispatch(WorkbenchActions.loadProject(projectMatch.params.id));
+        store.dispatch(openProjectPanel(projectMatch.params.id));
     } else if (collectionMatch) {
         store.dispatch(WorkbenchActions.loadCollection(collectionMatch.params.id));
     } else if (favoriteMatch) {
index cdbe8bab22dfc3b7cd43c382635a6b768c1445ce..fc6033002dccea2532e27dfe361eaa354747f860 100644 (file)
@@ -56,7 +56,7 @@ export class GroupsService<
             select: select
                 ? JSON.stringify(select.map(sel => {
                     const sp = sel.split(".");
-                    return sp.length == 2 ? (sp[0] + "." + snakeCase(sp[1])) : snakeCase(sel);
+                    return sp.length === 2 ? (sp[0] + "." + snakeCase(sp[1])) : snakeCase(sel);
                 }))
                 : undefined
         };
index 3b30f4aab5e3577fdc5c3f3632b7fb0dd60f9f50..7ad18b67bdb2f50b5baf1e4a3818abc3d4581d37 100644 (file)
@@ -7,6 +7,7 @@ import { bindDataExplorerActions } from "store/data-explorer/data-explorer-actio
 import { propertiesActions } from "store/properties/properties-actions";
 import { RootState } from 'store/store';
 import { getProperty } from "store/properties/properties";
+import { loadProject } from "store/workbench/workbench-actions";
 
 export const PROJECT_PANEL_ID = "projectPanel";
 export const PROJECT_PANEL_CURRENT_UUID = "projectPanelCurrentUuid";
@@ -14,7 +15,8 @@ export const IS_PROJECT_PANEL_TRASHED = 'isProjectPanelTrashed';
 export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
 
 export const openProjectPanel = (projectUuid: string) =>
-    (dispatch: Dispatch) => {
+    async (dispatch: Dispatch) => {
+        await dispatch<any>(loadProject(projectUuid));
         dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
         dispatch(projectPanelActions.RESET_EXPLORER_SEARCH_VALUE());
         dispatch(projectPanelActions.REQUEST_ITEMS());
index 1501fd4fb5be80db4e03d9f832e59116ec95b6f9..ec673d62239c49170ce2c0f8bbc06a270f15ca00 100644 (file)
@@ -77,17 +77,7 @@ import { MiddlewareListReducer } from 'common/plugintypes';
 import { tooltipsMiddleware } from './tooltips/tooltips-middleware';
 import { sidePanelReducer } from './side-panel/side-panel-reducer'
 import { bannerReducer } from './banner/banner-reducer';
-
-declare global {
-    interface Window {
-        __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
-    }
-}
-
-const composeEnhancers =
-    (process.env.NODE_ENV === 'development' &&
-        window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
-    compose;
+import { composeWithDevTools } from 'redux-devtools-extension';
 
 export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
 
@@ -187,7 +177,7 @@ export function configureStore(history: History, services: ServiceRepository, co
 
     middlewares = pluginConfig.middlewares.reduce(reduceMiddlewaresFn, middlewares);
 
-    const enhancer = composeEnhancers(applyMiddleware(redirectToMiddleware, ...middlewares));
+    const enhancer = composeWithDevTools({/* options */ })(applyMiddleware(redirectToMiddleware, ...middlewares));
     return createStore(rootReducer, enhancer);
 }
 
index 524337796efe37a6cfc472c9174eb3a7bd6012bd..a3c3a0969b2185224b340e7eabe2c9211a629568 100644 (file)
@@ -13,7 +13,6 @@ import {
 } from 'store/favorite-panel/favorite-panel-action';
 import {
     getProjectPanelCurrentUuid,
-    openProjectPanel,
     projectPanelActions,
     setIsProjectPanelTrashed,
 } from 'store/project-panel/project-panel-action';
@@ -866,7 +865,6 @@ const finishLoadingProject =
     (project: GroupContentsResource | string) =>
         async (dispatch: Dispatch<any>) => {
             const uuid = typeof project === 'string' ? project : project.uuid;
-            dispatch(openProjectPanel(uuid));
             dispatch(loadDetailsPanel(uuid));
             if (typeof project !== 'string') {
                 dispatch(updateResources([project]));
index 580aa8ed9243da6ecf6049fcdb91fb64d35b502a..e96e7751cd45de415b7a6bb7b7b5413d315c333f 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -3811,6 +3811,7 @@ __metadata:
     react-window: 1.8.5
     redux: 4.0.3
     redux-devtools: 3.4.1
+    redux-devtools-extension: ^2.13.9
     redux-form: 7.4.2
     redux-mock-store: 1.5.4
     redux-thunk: 2.3.0
@@ -15448,6 +15449,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"redux-devtools-extension@npm:^2.13.9":
+  version: 2.13.9
+  resolution: "redux-devtools-extension@npm:2.13.9"
+  peerDependencies:
+    redux: ^3.1.0 || ^4.0.0
+  checksum: 603d48fd6acf3922ef373b251ab3fdbb990035e90284191047b29d25b06ea18122bc4ef01e0704ccae495acb27ab5e47b560937e98213605dd88299470025db9
+  languageName: node
+  linkType: hard
+
 "redux-devtools-instrument@npm:^1.0.1":
   version: 1.10.0
   resolution: "redux-devtools-instrument@npm:1.10.0"