Merge branch '18984-project-type-filters-2' into main. Closes #18984
authorStephen Smith <stephen@curii.com>
Tue, 7 Jun 2022 21:37:00 +0000 (17:37 -0400)
committerStephen Smith <stephen@curii.com>
Tue, 7 Jun 2022 21:37:00 +0000 (17:37 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

18 files changed:
cypress/integration/collection.spec.js
cypress/integration/favorites.spec.js
cypress/integration/search.spec.js
src/components/collection-panel-files/collection-panel-files.tsx
src/store/collection-panel/collection-panel-action.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/store/collection-panel/collection-panel-reducer.ts
src/store/collections/collection-update-actions.ts
src/store/collections/collection-upload-actions.ts
src/store/workbench/workbench-actions.ts
src/store/workflow-panel/workflow-panel-actions.ts
src/views-components/collection-panel-files/collection-panel-files.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/details-panel/project-details.tsx
src/views-components/details-panel/workflow-details.tsx
src/views/collection-panel/collection-panel.tsx
src/views/process-panel/process-details-attributes.tsx
src/views/project-panel/project-panel.tsx

index b62a34414fb58b57c4e6ac0d418f14dbce78553f..0b06e53e1ab868fc71bec1b44c9425b3b168d0cd 100644 (file)
@@ -261,7 +261,7 @@ describe('Collection panel tests', function () {
                         });
                         // Test context menus
                         cy.get('[data-cy=collection-files-panel]')
-                            .contains(fileName).rightclick({ force: true });
+                            .contains(fileName).rightclick();
                         cy.get('[data-cy=context-menu]')
                             .should('contain', 'Download')
                             .and('not.contain', 'Open in new tab')
@@ -270,7 +270,7 @@ describe('Collection panel tests', function () {
                             .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
                         cy.get('body').click(); // Collapse the menu
                         cy.get('[data-cy=collection-files-panel]')
-                            .contains(subDirName).rightclick({ force: true });
+                            .contains(subDirName).rightclick();
                         cy.get('[data-cy=context-menu]')
                             .should('not.contain', 'Download')
                             .and('not.contain', 'Open in new tab')
@@ -368,7 +368,7 @@ describe('Collection panel tests', function () {
 
                 ['subdir', 'G%C3%BCnter\'s%20file', 'table%&?*2'].forEach((subdir) => {
                     cy.get('[data-cy=collection-files-panel]')
-                        .contains('bar').rightclick({force: true});
+                        .contains('bar').rightclick();
                     cy.get('[data-cy=context-menu]')
                         .contains('Rename')
                         .click();
@@ -381,9 +381,9 @@ describe('Collection panel tests', function () {
                     cy.get('[data-cy=collection-files-panel]')
                         .should('not.contain', 'bar')
                         .and('contain', subdir);
-                    cy.wait(1000);
                     cy.get('[data-cy=collection-files-panel]').contains(subdir).click();
-                    // Rename 'subdir/foo' to 'foo'
+
+                    // Rename 'subdir/foo' to 'bar'
                     cy.wait(1000);
                     cy.get('[data-cy=collection-files-panel]')
                         .contains('foo').rightclick();
@@ -399,7 +399,6 @@ describe('Collection panel tests', function () {
                         });
                     cy.get('[data-cy=form-submit-btn]').click();
 
-                    cy.wait(1000);
                     cy.get('[data-cy=collection-files-panel]')
                         .contains('Home')
                         .click();
@@ -1034,6 +1033,14 @@ describe('Collection panel tests', function () {
 
                     cy.goToPath(`/collections/${testCollection1.uuid}`);
 
+                    // Confirm initial collection state.
+                    cy.get('[data-cy=collection-files-panel]')
+                        .contains('bar').should('exist');
+                    cy.get('[data-cy=collection-files-panel]')
+                        .contains('5mb_a.bin').should('not.exist');
+                    cy.get('[data-cy=collection-files-panel]')
+                        .contains('5mb_b.bin').should('not.exist');
+
                     cy.get('[data-cy=upload-button]').click();
 
                     cy.fixture('files/5mb.bin', 'base64').then(content => {
@@ -1043,9 +1050,25 @@ describe('Collection panel tests', function () {
                         cy.get('[data-cy=form-submit-btn]').click();
 
                         cy.get('button[aria-label=Remove]').should('exist');
-                        cy.get('button[aria-label=Remove]').click({ multiple: true, force: true });
+                        cy.get('button[aria-label=Remove]')
+                            .click({ multiple: true, force: true });
 
                         cy.get('[data-cy=form-submit-btn]').should('not.exist');
+
+                        // Confirm final collection state.
+                        cy.get('[data-cy=collection-files-panel]')
+                            .contains('bar').should('exist');
+                        // The following fails, but doesn't seem to happen
+                        // in the real world. Maybe there's a race between
+                        // the PUT request finishing and the 'Remove' button
+                        // dissapearing, because sometimes just one of the 2
+                        // files gets uploaded.
+                        // Maybe this will be needed to simulate a slow network:
+                        // https://docs.cypress.io/api/commands/intercept#Convenience-functions-1
+                        // cy.get('[data-cy=collection-files-panel]')
+                        //     .contains('5mb_a.bin').should('not.exist');
+                        // cy.get('[data-cy=collection-files-panel]')
+                        //     .contains('5mb_b.bin').should('not.exist');
                     });
                 });
         });
index 105657effa77300136db29760437731eec3ec764..7fd091245f770015a7c86c12ae938d0ace54db86 100644 (file)
@@ -64,7 +64,7 @@ describe('Favorites tests', function () {
                 cy.loginAs(activeUser);
                 cy.goToPath(`/collections/${testSourceCollection.uuid}`);
                 cy.get('[data-cy=collection-files-panel]').contains('bar');
-                cy.get('[data-cy=collection-files-panel]').find('input[type=checkbox]').click({ force: true });
+                cy.get('[data-cy=collection-files-panel]').find('input[type=checkbox]').click();
                 cy.get('[data-cy=collection-files-panel-options-btn]').click();
                 cy.get('[data-cy=context-menu]')
                     .contains('Copy selected into the collection').click();
index 491292bece641a79e977974faccafc0fea7b6379..5434ca248a5167e5461014491d77f422980ae693 100644 (file)
@@ -104,4 +104,26 @@ describe('Search tests', function() {
             cy.get('[data-cy=element-path]').should('contain', `/ Projects / ${colName}`);
         });
     });
+
+    it('can display owner of the item', function() {
+        const colName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+
+        cy.createCollection(adminUser.token, {
+            name: colName,
+            owner_uuid: activeUser.user.uuid,
+            preserve_version: true,
+            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+        }).then(function() {
+            cy.loginAs(activeUser);
+
+            cy.doSearch(colName);
+
+            cy.get('[data-cy=search-results]').should('contain', colName);
+
+            cy.get('[data-cy=search-results]').contains(colName).closest('tr')
+                .within(() => {
+                    cy.get('p').contains(activeUser.user.uuid).should('contain', activeUser.user.full_name);
+                });
+        });
+    });
 });
\ No newline at end of file
index 05b493636da7aad97474cc3d61b3fafdfe97e0f3..42408270c0b67bbde362261a97699b7b8a2fa064 100644 (file)
@@ -10,24 +10,38 @@ import AutoSizer from "react-virtualized-auto-sizer";
 import servicesProvider from 'common/service-provider';
 import { CustomizeTableIcon, DownloadIcon } from 'components/icon/icon';
 import { SearchInput } from 'components/search-input/search-input';
-import { ListItemIcon, StyleRulesCallback, Theme, WithStyles, withStyles, Tooltip, IconButton, Checkbox, CircularProgress, Button } from '@material-ui/core';
+import {
+    ListItemIcon,
+    StyleRulesCallback,
+    Theme,
+    WithStyles,
+    withStyles,
+    Tooltip,
+    IconButton,
+    Checkbox,
+    CircularProgress,
+    Button,
+} from '@material-ui/core';
 import { FileTreeData } from '../file-tree/file-tree-data';
 import { TreeItem, TreeItemStatus } from '../tree/tree';
 import { RootState } from 'store/store';
 import { WebDAV, WebDAVRequestConfig } from 'common/webdav';
 import { AuthState } from 'store/auth/auth-reducer';
 import { extractFilesData } from 'services/collection-service/collection-service-files-response';
-import { DefaultIcon, DirectoryIcon, FileIcon, BackIcon, SidePanelRightArrowIcon } from 'components/icon/icon';
+import {
+    DefaultIcon,
+    DirectoryIcon,
+    FileIcon,
+    BackIcon,
+    SidePanelRightArrowIcon
+} from 'components/icon/icon';
 import { setCollectionFiles } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions';
 import { sortBy } from 'lodash';
 import { formatFileSize } from 'common/formatters';
 import { getInlineFileUrl, sanitizeToken } from 'views-components/context-menu/actions/helpers';
 
 export interface CollectionPanelFilesProps {
-    items: any;
     isWritable: boolean;
-    isLoading: boolean;
-    tooManyFiles: boolean;
     onUploadDataClick: (targetLocation?: string) => void;
     onSearchChange: (searchValue: string) => void;
     onItemMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>, isWritable: boolean) => void;
@@ -35,14 +49,35 @@ export interface CollectionPanelFilesProps {
     onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
     onCollapseToggle: (id: string, status: TreeItemStatus) => void;
     onFileClick: (id: string) => void;
-    loadFilesFunc: () => void;
     currentItemUuid: any;
     dispatch: Function;
     collectionPanelFiles: any;
     collectionPanel: any;
 }
 
-type CssRules = "backButton" | "backButtonHidden" | "pathPanelPathWrapper" | "uploadButton" | "uploadIcon" | "loader" | "wrapper" | "dataWrapper" | "row" | "rowEmpty" | "leftPanel" | "rightPanel" | "pathPanel" | "pathPanelItem" | "rowName" | "listItemIcon" | "rowActive" | "pathPanelMenu" | "rowSelection" | "leftPanelHidden" | "leftPanelVisible" | "searchWrapper" | "searchWrapperHidden";
+type CssRules = "backButton"
+    | "backButtonHidden"
+    | "pathPanelPathWrapper"
+    | "uploadButton"
+    | "uploadIcon"
+    | "loader"
+    | "wrapper"
+    | "dataWrapper"
+    | "row"
+    | "rowEmpty"
+    | "leftPanel"
+    | "rightPanel"
+    | "pathPanel"
+    | "pathPanelItem"
+    | "rowName"
+    | "listItemIcon"
+    | "rowActive"
+    | "pathPanelMenu"
+    | "rowSelection"
+    | "leftPanelHidden"
+    | "leftPanelVisible"
+    | "searchWrapper"
+    | "searchWrapperHidden";
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     wrapper: {
@@ -198,8 +233,8 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
     };
 
     const parentRef = React.useRef(null);
-    const [path, setPath]: any = React.useState([]);
-    const [pathData, setPathData]: any = React.useState({});
+    const [path, setPath] = React.useState<string[]>([]);
+    const [pathData, setPathData] = React.useState({});
     const [isLoading, setIsLoading] = React.useState(false);
     const [leftSearch, setLeftSearch] = React.useState('');
     const [rightSearch, setRightSearch] = React.useState('');
@@ -220,12 +255,12 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
     const fetchData = (keys, ignoreCache = false) => {
         const keyArray = Array.isArray(keys) ? keys : [keys];
 
-        Promise.all(keyArray
+        Promise.all(keyArray.filter(key => !!key)
             .map((key) => {
                 const dataExists = !!pathData[key];
                 const runningRequest = pathPromise[key];
 
-                if ((!dataExists || ignoreCache) && (!runningRequest || ignoreCache)) {
+                if (ignoreCache || (!dataExists && !runningRequest)) {
                     if (!isLoading) {
                         setIsLoading(true);
                     }
@@ -239,34 +274,34 @@ 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 };
-                }, {});
+        .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;
+                    });
 
-                setPathData({ ...pathData, ...newState });
-            })
-            .finally(() => {
-                setIsLoading(false);
-                keyArray.forEach(key => delete pathPromise[key]);
-            });
+                    return { [key]: sortedResult };
+                }
+                return {};
+            }).reduce((prev, next) => {
+                return { ...next, ...prev };
+            }, {});
+
+            setPathData({ ...pathData, ...newState });
+        })
+        .finally(() => {
+            setIsLoading(false);
+            keyArray.forEach(key => delete pathPromise[key]);
+        });
     };
 
     React.useEffect(() => {
@@ -280,7 +315,12 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
     const currentPDH = (collectionPanel.item || {}).portableDataHash;
     React.useEffect(() => {
         if (currentPDH) {
-            fetchData([leftKey, rightKey], true);
+            // Avoid fetching the same content level twice
+            if (leftKey !== rightKey) {
+                fetchData([leftKey, rightKey], true);
+            } else {
+                fetchData(rightKey, true);
+            }
         }
     }, [currentPDH]); // eslint-disable-line react-hooks/exhaustive-deps
 
@@ -315,13 +355,12 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                 onItemMenuOpen(event, item, isWritable);
             }
         },
-        [onItemMenuOpen, isWritable, rightData] // eslint-disable-line react-hooks/exhaustive-deps
-    );
+        [onItemMenuOpen, isWritable, rightData]);
 
     React.useEffect(() => {
         let node = null;
 
-        if (parentRef && parentRef.current) {
+        if (parentRef?.current) {
             node = parentRef.current;
             (node as any).addEventListener('contextmenu', handleRightClick);
         }
@@ -419,149 +458,107 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
         [props.onOptionsMenuOpen] // eslint-disable-line react-hooks/exhaustive-deps
     );
 
-    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>)
-                    }
-                </div>
-                <Tooltip className={classes.pathPanelMenu} title="More options" disableFocusListener>
-                    <IconButton
-                        data-cy='collection-files-panel-options-btn'
-                        onClick={(ev) => {
-                            onOptionsMenuOpen(ev, isWritable);
-                        }}>
-                        <CustomizeTableIcon />
+    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>)
+            }
+            </div>
+            <Tooltip className={classes.pathPanelMenu} title="More options" disableFocusListener>
+                <IconButton data-cy='collection-files-panel-options-btn'
+                    onClick={(ev) => {
+                        onOptionsMenuOpen(ev, isWritable);
+                    }}>
+                    <CustomizeTableIcon />
+                </IconButton>
+            </Tooltip>
+        </div>
+        <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([...path.slice(0, path.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>
+                    }}
+                </AutoSizer>
+                : <div className={classes.row}><CircularProgress className={classes.loader} size={30} /></div> }
+                </div>
             </div>
-            <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([...path.slice(0, path.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>
-                                    }}
-                                </AutoSizer> : <div className={classes.row}><CircularProgress className={classes.loader} size={30} /></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>
-                <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 ?
-                                <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}
-                                                        </div>
-                                                        <span className={classes.rowName} style={{ marginLeft: 'auto', marginRight: '1rem' }}>
-                                                            {formatFileSize(size)}
-                                                        </span>
-                                                    </div>
-                                                }
-                                            }
-                                        </FixedSizeList> : <div className={classes.rowEmpty}>This collection is empty</div>
-                                    }}
-                                </AutoSizer> : <div className={classes.row}><CircularProgress className={classes.loader} size={30} /></div>
-                        }
-                    </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
+                    ? <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}
+                                    </div>
+                                    <span className={classes.rowName} style={{
+                                        marginLeft: 'auto', marginRight: '1rem' }}>
+                                        { formatFileSize(size) }
+                                    </span>
+                                </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>}));
index c50ff6a888253469df4a0929018bce7f14d7a435..7bab86320da1e00c2a3f2a1706b824722c87e14c 100644 (file)
@@ -3,9 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import {
-    COLLECTION_PANEL_LOAD_FILES_THRESHOLD
-} from "./collection-panel-files/collection-panel-files-actions";
 import { CollectionResource } from 'models/collection';
 import { RootState } from "store/store";
 import { ServiceRepository } from "services/services";
@@ -18,8 +15,6 @@ import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
 
 export const collectionPanelActions = unionize({
     SET_COLLECTION: ofType<CollectionResource>(),
-    LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>(),
-    LOAD_BIG_COLLECTIONS: ofType<boolean>(),
 });
 
 export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
@@ -27,15 +22,15 @@ export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
 export const loadCollectionPanel = (uuid: string, forceReload = false) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { collectionPanel: { item } } = getState();
-        const collection = (item && item.uuid === uuid && !forceReload)
-            ? item
-            : await services.collectionService.get(uuid);
-        dispatch<any>(loadDetailsPanel(collection.uuid));
-        dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection }));
-        dispatch(resourcesActions.SET_RESOURCES([collection]));
-        if (collection.fileCount <= COLLECTION_PANEL_LOAD_FILES_THRESHOLD &&
-            !getState().collectionPanel.loadBigCollections) {
+        let collection: CollectionResource | null = null;
+        if (!item || item.uuid !== uuid || forceReload) {
+            collection = await services.collectionService.get(uuid);
+            dispatch(collectionPanelActions.SET_COLLECTION(collection));
+            dispatch(resourcesActions.SET_RESOURCES([collection]));
+        } else {
+            collection = item;
         }
+        dispatch<any>(loadDetailsPanel(collection.uuid));
         return collection;
     };
 
index 71e1f6e8eed4b02343552843a746c29d10494a1b..8c5e5b5a67df1f345ccd0ef645b7ec2e25e858c0 100644 (file)
@@ -15,8 +15,6 @@ import { filterCollectionFilesBySelection } from './collection-panel-files-state
 import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
 import { getDialog } from "store/dialog/dialog-reducer";
 import { getFileFullPath, sortFilesTree } from "services/collection-service/collection-service-files-response";
-import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
-import { loadCollectionPanel } from "../collection-panel-action";
 
 export const collectionPanelFilesAction = unionize({
     SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
@@ -30,7 +28,6 @@ export const collectionPanelFilesAction = unionize({
 export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
 
 export const COLLECTION_PANEL_LOAD_FILES = 'collectionPanelLoadFiles';
-export const COLLECTION_PANEL_LOAD_FILES_THRESHOLD = 40000;
 
 export const setCollectionFiles = (files, joinParents = true) => (dispatch: any) => {
     const tree = createCollectionFilesTree(files, joinParents);
@@ -39,33 +36,11 @@ export const setCollectionFiles = (files, joinParents = true) => (dispatch: any)
     dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(mapped));
 };
 
-export const loadCollectionFiles = (uuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PANEL_LOAD_FILES));
-        services.collectionService.files(uuid).then(files => {
-            // Given the array of directories and files, create the appropriate tree nodes,
-            // sort them, and add the complete url to each.
-            const tree = createCollectionFilesTree(files);
-            const sorted = sortFilesTree(tree);
-            const mapped = mapTreeValues(services.collectionService.extendFileURL)(sorted);
-            dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(mapped));
-            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PANEL_LOAD_FILES));
-        }).catch(() => {
-            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PANEL_LOAD_FILES));
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: `Error getting file list`,
-                hideDuration: 2000,
-                kind: SnackbarKind.ERROR
-            }));
-        });
-    };
-
 export const removeCollectionFiles = (filePaths: string[]) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
             services.collectionService.deleteFiles(currentCollection.uuid, filePaths).then(() => {
-                dispatch<any>(loadCollectionPanel(currentCollection.uuid, true));
                 dispatch(snackbarActions.OPEN_SNACKBAR({
                     message: 'Removed.',
                     hideDuration: 2000,
@@ -155,7 +130,6 @@ export const renameFile = (newFullPath: string) =>
                 const oldPath = getFileFullPath(file);
                 const newPath = newFullPath;
                 services.collectionService.moveFile(currentCollection.uuid, oldPath, newPath).then(() => {
-                    dispatch<any>(loadCollectionPanel(currentCollection.uuid, true));
                     dispatch(dialogActions.CLOSE_DIALOG({ id: RENAME_FILE_DIALOG }));
                     dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'File name changed.', hideDuration: 2000 }));
                 }).catch(e => {
index a6aa87bdf387eea187e892384f00eab6bb95ac61..6afba66c4ca8854d8bcfbdd8cb24d2900cbdd0df 100644 (file)
@@ -7,12 +7,10 @@ import { CollectionResource } from "models/collection";
 
 export interface CollectionPanelState {
     item: CollectionResource | null;
-    loadBigCollections: boolean;
 }
 
 const initialState = {
     item: null,
-    loadBigCollections: false,
 };
 
 export const collectionPanelReducer = (state: CollectionPanelState = initialState, action: CollectionPanelAction) =>
@@ -21,8 +19,5 @@ export const collectionPanelReducer = (state: CollectionPanelState = initialStat
         SET_COLLECTION: (item) => ({
              ...state,
              item,
-             loadBigCollections: false,
         }),
-        LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item }),
-        LOAD_BIG_COLLECTIONS: (loadBigCollections) => ({ ...state, loadBigCollections}),
     });
index 82418d27abe75b8bbeef49b07132151ef52187bb..bf9c64492d79cef6a5f6a75708436866a9700e0b 100644 (file)
@@ -55,7 +55,7 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
             properties: collection.properties }
         ).then(updatedCollection => {
             updatedCollection = {...cachedCollection, ...updatedCollection};
-            dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
+            dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
             dispatch(snackbarActions.OPEN_SNACKBAR({
index 135538b074dbee437cebf55b5b9daf4ab4d1ecc8..e9c5cc35b873f2640681d3c5df521d6ceffbf613 100644 (file)
@@ -6,14 +6,10 @@ import { Dispatch } from 'redux';
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
 import { dialogActions } from 'store/dialog/dialog-actions';
-import { loadCollectionFiles } from '../collection-panel/collection-panel-files/collection-panel-files-actions';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { fileUploaderActions } from 'store/file-uploader/file-uploader-actions';
 import { reset, startSubmit, stopSubmit } from 'redux-form';
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
-import { collectionPanelFilesAction } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions';
-import { createTree } from 'models/tree';
-import { loadCollectionPanel } from '../collection-panel/collection-panel-action';
 import * as WorkbenchActions from 'store/workbench/workbench-actions';
 
 export const uploadCollectionFiles = (collectionUuid: string, targetLocation?: string) =>
@@ -40,10 +36,7 @@ export const submitCollectionFiles = (targetLocation?: string) =>
             try {
                 dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
                 dispatch(startSubmit(COLLECTION_UPLOAD_FILES_DIALOG));
-                await dispatch<any>(uploadCollectionFiles(currentCollection.uuid, targetLocation))
-                    .then(() => dispatch<any>(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() })));
-                dispatch<any>(loadCollectionFiles(currentCollection.uuid));
-                dispatch<any>(loadCollectionPanel(currentCollection.uuid));
+                await dispatch<any>(uploadCollectionFiles(currentCollection.uuid, targetLocation));
                 dispatch(closeUploadCollectionFilesDialog());
                 dispatch(snackbarActions.OPEN_SNACKBAR({
                     message: 'Data has been uploaded.',
index d2ff84b3acd447f347d067e730305fd9d0910b0e..0a3484310ee74a5d6e5182f3e47fb27290520ef3 100644 (file)
@@ -100,8 +100,6 @@ import { subprocessPanelActions } from 'store/subprocess-panel/subprocess-panel-
 import { subprocessPanelColumns } from 'views/subprocess-panel/subprocess-panel-root';
 import { loadAllProcessesPanel, allProcessesPanelActions } from '../all-processes-panel/all-processes-panel-action';
 import { allProcessesPanelColumns } from 'views/all-processes-panel/all-processes-panel';
-import { collectionPanelFilesAction } from '../collection-panel/collection-panel-files/collection-panel-files-actions';
-import { createTree } from 'models/tree';
 import { AdminMenuIcon } from 'components/icon/icon';
 import { userProfileGroupsColumns } from 'views/user-profile-panel/user-profile-panel-root';
 
@@ -295,11 +293,9 @@ export const loadCollection = (uuid: string) =>
         async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
             const userUuid = getUserUuid(getState());
             if (userUuid) {
-                // Clear collection files panel
-                dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
                 const match = await loadGroupContentsResource({ uuid, userUuid, services });
                 match({
-                    OWNED: async collection => {
+                    OWNED: collection => {
                         dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
                         dispatch(updateResources([collection]));
                         dispatch(activateSidePanelTreeItem(collection.ownerUuid));
index 7c90fa6bb290fbfd6f58aa16f96b2876effc0b88..912f76308ceac33cb6865efdc3aeccb2322695e1 100644 (file)
@@ -9,13 +9,18 @@ import { bindDataExplorerActions } from 'store/data-explorer/data-explorer-actio
 import { propertiesActions } from 'store/properties/properties-actions';
 import { getProperty } from 'store/properties/properties';
 import { navigateToRunProcess } from 'store/navigation/navigation-action';
-import { goToStep, runProcessPanelActions, loadPresets, getWorkflowRunnerSettings } from 'store/run-process-panel/run-process-panel-actions';
+import {
+    goToStep,
+    runProcessPanelActions,
+    loadPresets,
+    getWorkflowRunnerSettings
+} from 'store/run-process-panel/run-process-panel-actions';
 import { snackbarActions } from 'store/snackbar/snackbar-actions';
 import { initialize } from 'redux-form';
 import { RUN_PROCESS_BASIC_FORM } from 'views/run-process-panel/run-process-basic-form';
 import { RUN_PROCESS_INPUTS_FORM } from 'views/run-process-panel/run-process-inputs-form';
 import { RUN_PROCESS_ADVANCED_FORM } from 'views/run-process-panel/run-process-advanced-form';
-import { getResource, ResourcesState } from 'store/resources/resources';
+import { getResource } from 'store/resources/resources';
 import { ProjectResource } from 'models/project';
 import { UserResource } from 'models/user';
 import { getUserUuid } from "common/getuser";
index 216ec66967be4c3ab7085dfff7970fd18c309549..a26b9fe3ee0ad65ab2cc1d6b018de9b5783e90fb 100644 (file)
@@ -8,41 +8,17 @@ import {
     CollectionPanelFilesProps
 } from "components/collection-panel-files/collection-panel-files";
 import { RootState } from "store/store";
-import { TreeItemStatus } from "components/tree/tree";
-import { VirtualTreeItem as TreeItem } from "components/tree/virtual-tree";
-import {
-    CollectionPanelDirectory,
-    CollectionPanelFile,
-    CollectionPanelFilesState
-} from "store/collection-panel/collection-panel-files/collection-panel-files-state";
-import { FileTreeData } from "components/file-tree/file-tree-data";
 import { Dispatch } from "redux";
 import { collectionPanelFilesAction } from "store/collection-panel/collection-panel-files/collection-panel-files-actions";
 import { ContextMenuKind } from "../context-menu/context-menu";
-import { getNode, getNodeChildrenIds, Tree, TreeNode, initTreeNode } from "models/tree";
-import { CollectionFileType, createCollectionDirectory } from "models/collection-file";
 import { openContextMenu, openCollectionFilesContextMenu } from 'store/context-menu/context-menu-actions';
 import { openUploadCollectionFilesDialog } from 'store/collections/collection-upload-actions';
 import { ResourceKind } from "models/resource";
 import { openDetailsPanel } from 'store/details-panel/details-panel-action';
 
-const memoizedMapStateToProps = () => {
-    let prevState: CollectionPanelFilesState;
-    let prevTree: Array<TreeItem<FileTreeData>>;
-
-    return (state: RootState): Pick<CollectionPanelFilesProps, "items" | "currentItemUuid"> => {
-        if (prevState !== state.collectionPanelFiles) {
-            prevState = state.collectionPanelFiles;
-            prevTree = [].concat.apply(
-                [], getNodeChildrenIds('')(state.collectionPanelFiles)
-                    .map(collectionItemToList(0)(state.collectionPanelFiles)));
-        }
-        return {
-            items: prevTree,
-            currentItemUuid: state.detailsPanel.resourceUuid
-        };
-    };
-};
+const mapStateToProps = (state: RootState): Pick<CollectionPanelFilesProps, "currentItemUuid"> => ({
+    currentItemUuid: state.detailsPanel.resourceUuid
+});
 
 const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onSearchChange' | 'onFileClick' | 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
     onUploadDataClick: (targetLocation?: string) => {
@@ -84,43 +60,4 @@ const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps,
     },
 });
 
-export const CollectionPanelFiles = connect(memoizedMapStateToProps(), mapDispatchToProps)(Component);
-
-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 treeItem = {
-            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: [], // Not used in this case as we're converting a tree to a list.
-            itemCount: node.children.length,
-            open: node.value.type === CollectionFileType.DIRECTORY ? !node.value.collapsed : false,
-            selected: node.value.selected,
-            status: TreeItemStatus.LOADED,
-            level,
-        };
-
-        const treeItemChilds = treeItem.open
-            ? [].concat.apply([], node.children.map(collectionItemToList(level+1)(tree)))
-            : [];
-
-        return [
-            treeItem,
-            ...treeItemChilds,
-        ];
-    };
+export const CollectionPanelFiles = connect(mapStateToProps, mapDispatchToProps)(Component);
index cd9f972e249a32e6111bc17de17fab8b8c7e0b45..7822bdc6b4cd2411aaa37fc78a6a626200db35cf 100644 (file)
@@ -184,22 +184,22 @@ export const ResourceLastName = connect(
         return resource || { lastName: '' };
     })(renderLastName);
 
-const renderFullName = (dispatch: Dispatch ,item: { uuid: string, firstName: string, lastName: string }, link?: boolean) => {
+const renderFullName = (dispatch: Dispatchitem: { uuid: string, firstName: string, lastName: string }, link?: boolean) => {
     const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
     return link ? <Typography noWrap
         color="primary"
         style={{ 'cursor': 'pointer' }}
         onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
-            {displayName}
+        {displayName}
     </Typography> :
-    <Typography noWrap>{displayName}</Typography>;
+        <Typography noWrap>{displayName}</Typography>;
 }
 
 export const UserResourceFullName = connect(
     (state: RootState, props: { uuid: string, link?: boolean }) => {
         const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return {item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link};
-    })((props: {item: {uuid: string, firstName: string, lastName: string}, link?: boolean} & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
+        return { item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link };
+    })((props: { item: { uuid: string, firstName: string, lastName: string }, link?: boolean } & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
 
 const renderUuid = (item: { uuid: string }) =>
     <Typography data-cy="uuid" noWrap>
@@ -208,8 +208,8 @@ const renderUuid = (item: { uuid: string }) =>
     </Typography>;
 
 export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
-        getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' }
-    ))(renderUuid);
+    getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' }
+))(renderUuid);
 
 const renderEmail = (item: { email: string }) =>
     <Typography noWrap>{item.email}</Typography>;
@@ -227,17 +227,17 @@ enum UserAccountStatus {
     UNKNOWN = ''
 }
 
-const renderAccountStatus = (props: {status: UserAccountStatus}) =>
+const renderAccountStatus = (props: { status: UserAccountStatus }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={8} data-cy="account-status">
         <Grid item>
             {(() => {
-                switch(props.status) {
+                switch (props.status) {
                     case UserAccountStatus.ACTIVE:
-                        return <ActiveIcon style={{color: '#4caf50', verticalAlign: "middle"}} />;
+                        return <ActiveIcon style={{ color: '#4caf50', verticalAlign: "middle" }} />;
                     case UserAccountStatus.SETUP:
-                        return <SetupIcon style={{color: '#2196f3', verticalAlign: "middle"}} />;
+                        return <SetupIcon style={{ color: '#2196f3', verticalAlign: "middle" }} />;
                     case UserAccountStatus.INACTIVE:
-                        return <InactiveIcon style={{color: '#9e9e9e', verticalAlign: "middle"}} />;
+                        return <InactiveIcon style={{ color: '#9e9e9e', verticalAlign: "middle" }} />;
                     default:
                         return <></>;
                 }
@@ -262,37 +262,37 @@ const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
     )(state.resources);
 
     if (user) {
-        return user.isActive ? {status: UserAccountStatus.ACTIVE} : permissions.length > 0 ? {status: UserAccountStatus.SETUP} : {status: UserAccountStatus.INACTIVE};
+        return user.isActive ? { status: UserAccountStatus.ACTIVE } : permissions.length > 0 ? { status: UserAccountStatus.SETUP } : { status: UserAccountStatus.INACTIVE };
     } else {
-        return {status: UserAccountStatus.UNKNOWN};
+        return { status: UserAccountStatus.UNKNOWN };
     }
 }
 
 export const ResourceLinkTailAccountStatus = connect(
     (state: RootState, props: { uuid: string }) => {
         const link = getResource<LinkResource>(props.uuid)(state.resources);
-        return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, {uuid: link.tailUuid}) : {status: UserAccountStatus.UNKNOWN};
+        return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN };
     })(renderAccountStatus);
 
 export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
 
 const renderIsHidden = (props: {
-                            memberLinkUuid: string,
-                            permissionLinkUuid: string,
-                            visible: boolean,
-                            canManage: boolean,
-                            setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void
-                        }) => {
+    memberLinkUuid: string,
+    permissionLinkUuid: string,
+    visible: boolean,
+    canManage: boolean,
+    setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void
+}) => {
     if (props.memberLinkUuid) {
         return <Checkbox
-                data-cy="user-visible-checkbox"
-                color="primary"
-                checked={props.visible}
-                disabled={!props.canManage}
-                onClick={(e) => {
-                    e.stopPropagation();
-                    props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
-                }} />;
+            data-cy="user-visible-checkbox"
+            color="primary"
+            checked={props.visible}
+            disabled={!props.canManage}
+            onClick={(e) => {
+                e.stopPropagation();
+                props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
+            }} />;
     } else {
         return <Typography />;
     }
@@ -357,7 +357,7 @@ export const VirtualMachineHostname = connect(
         return resource || { hostname: '' };
     })(renderHostname);
 
-const renderVirtualMachineLogin = (login: {user: string}) =>
+const renderVirtualMachineLogin = (login: { user: string }) =>
     <Typography noWrap>{login.user}</Typography>
 
 export const VirtualMachineLogin = connect(
@@ -365,7 +365,7 @@ export const VirtualMachineLogin = connect(
         const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
         const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
 
-        return {user: user?.username || permission?.tailUuid || ''};
+        return { user: user?.username || permission?.tailUuid || '' };
     })(renderVirtualMachineLogin);
 
 // Common methods
@@ -442,7 +442,7 @@ export const ResourceLinkClass = connect(
 
 const getResourceDisplayName = (resource: Resource): string => {
     if ((resource as UserResource).kind === ResourceKind.USER
-          && typeof (resource as UserResource).firstName !== 'undefined') {
+        && typeof (resource as UserResource).firstName !== 'undefined') {
         // We can be sure the resource is UserResource
         return getUserDisplayName(resource as UserResource);
     } else {
@@ -516,7 +516,7 @@ const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boo
                 </IconButton>
             </Typography>;
     } else {
-      return <Typography noWrap></Typography>;
+        return <Typography noWrap></Typography>;
     }
 }
 
@@ -530,7 +530,7 @@ export const ResourceLinkDelete = connect(
             canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
         };
     })((props: { item: LinkResource, canManage: boolean } & DispatchProp<any>) =>
-      renderLinkDelete(props.dispatch, props.item, props.canManage));
+        renderLinkDelete(props.dispatch, props.item, props.canManage));
 
 export const ResourceLinkTailEmail = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -713,10 +713,17 @@ const userFromID =
             return { uuid: props.uuid, userFullname };
         });
 
-export const ResourceOwnerWithName =
+const ownerFromResourceId =
     compose(
-        userFromID,
-        withStyles({}, { withTheme: true }))
+        connect((state: RootState, props: { uuid: string }) => {
+            const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
+            return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
+        }),
+        userFromID
+    );
+
+const _resourceWithName =
+    withStyles({}, { withTheme: true })
         ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
             const { uuid, userFullname, dispatch, theme } = props;
 
@@ -732,6 +739,10 @@ export const ResourceOwnerWithName =
             </Typography>;
         });
 
+export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
+
+export const ResourceWithName = userFromID(_resourceWithName);
+
 export const UserNameFromID =
     compose(userFromID)(
         (props: { uuid: string, userFullname: string, dispatch: Dispatch }) => {
@@ -798,7 +809,7 @@ export const ResponsiblePerson =
 
             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
                 {responsiblePersonName} ({uuid})
-                </Typography>;
+            </Typography>;
         });
 
 const renderType = (type: string, subtype: string) =>
@@ -835,18 +846,18 @@ export const ProcessStatus = compose(
     withStyles({}, { withTheme: true }))
     ((props: { process?: Process, theme: ArvadosTheme }) =>
         props.process
-        ? <Chip label={getProcessStatus(props.process)}
-            style={{
-                height: props.theme.spacing.unit * 3,
-                width: props.theme.spacing.unit * 12,
-                backgroundColor: getProcessStatusColor(
-                    getProcessStatus(props.process), props.theme),
-                color: props.theme.palette.common.white,
-                fontSize: '0.875rem',
-                borderRadius: props.theme.spacing.unit * 0.625,
-            }}
-        />
-        : <Typography>-</Typography>
+            ? <Chip label={getProcessStatus(props.process)}
+                style={{
+                    height: props.theme.spacing.unit * 3,
+                    width: props.theme.spacing.unit * 12,
+                    backgroundColor: getProcessStatusColor(
+                        getProcessStatus(props.process), props.theme),
+                    color: props.theme.palette.common.white,
+                    fontSize: '0.875rem',
+                    borderRadius: props.theme.spacing.unit * 0.625,
+                }}
+            />
+            : <Typography>-</Typography>
     );
 
 export const ProcessStartDate = connect(
index d410076734dcd96e1f847b07bd3a862e7b8aded9..6d48e984de0f5ec7c84178c82b16c4e3a924918f 100644 (file)
@@ -16,7 +16,7 @@ import { withStyles, StyleRulesCallback, WithStyles, Button } from '@material-ui
 import { ArvadosTheme } from 'common/custom-theme';
 import { Dispatch } from 'redux';
 import { getPropertyChip } from '../resource-properties-form/property-chip';
-import { ResourceOwnerWithName } from '../data-explorer/renderers';
+import { ResourceWithName } from '../data-explorer/renderers';
 import { GroupClass } from "models/group";
 import { openProjectUpdateDialog, ProjectUpdateFormDialogData } from 'store/projects/project-update-actions';
 
@@ -41,7 +41,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         marginBottom: theme.spacing.unit / 2,
     },
     editIcon: {
-        paddingRight: theme.spacing.unit/2,
+        paddingRight: theme.spacing.unit / 2,
         fontSize: '1.125rem',
     },
     editButton: {
@@ -70,21 +70,21 @@ const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
     withStyles(styles)(
         ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
             {project.groupClass !== GroupClass.FILTER ?
-                    <Button onClick={onClick({
-                        uuid: project.uuid,
-                        name: project.name,
-                        description: project.description,
-                        properties: project.properties,
-                    })}
-                        className={classes.editButton} variant='contained'
-                        data-cy='details-panel-edit-btn' color='primary' size='small'>
-                        <RenameIcon className={classes.editIcon} /> Edit
-                    </Button>
-                    : ''
-                }
+                <Button onClick={onClick({
+                    uuid: project.uuid,
+                    name: project.name,
+                    description: project.description,
+                    properties: project.properties,
+                })}
+                    className={classes.editButton} variant='contained'
+                    data-cy='details-panel-edit-btn' color='primary' size='small'>
+                    <RenameIcon className={classes.editIcon} /> Edit
+                </Button>
+                : ''
+            }
             <DetailsAttribute label='Type' value={project.groupClass === GroupClass.FILTER ? 'Filter group' : resourceLabel(ResourceKind.PROJECT)} />
             <DetailsAttribute label='Owner' linkToUuid={project.ownerUuid}
-                uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+                uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
             <DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
             <DetailsAttribute label='Created at' value={formatDate(project.createdAt)} />
             <DetailsAttribute label='UUID' linkToUuid={project.uuid} value={project.uuid} />
@@ -101,9 +101,9 @@ const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
             {
                 Object.keys(project.properties).map(k =>
                     Array.isArray(project.properties[k])
-                    ? project.properties[k].map((v: string) =>
-                        getPropertyChip(k, v, undefined, classes.tag))
-                    : getPropertyChip(k, project.properties[k], undefined, classes.tag)
+                        ? project.properties[k].map((v: string) =>
+                            getPropertyChip(k, v, undefined, classes.tag))
+                        : getPropertyChip(k, project.properties[k], undefined, classes.tag)
                 )
             }
         </div>
index 7076823c7635d6d9c726804e155b788af8d44f31..98978dd279671eaf23a8ca174440208f4ffa1773 100644 (file)
@@ -3,12 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { DefaultIcon, WorkflowIcon } from 'components/icon/icon';
+import { WorkflowIcon } from 'components/icon/icon';
 import { WorkflowResource } from 'models/workflow';
 import { DetailsData } from "./details-data";
-import { DefaultView } from 'components/default-view/default-view';
 import { DetailsAttribute } from 'components/details-attribute/details-attribute';
-import { ResourceOwnerWithName } from 'views-components/data-explorer/renderers';
+import { ResourceWithName } from 'views-components/data-explorer/renderers';
 import { formatDate } from "common/formatters";
 import { Grid } from '@material-ui/core';
 import { withStyles, StyleRulesCallback, WithStyles, Button } from '@material-ui/core';
@@ -61,7 +60,7 @@ export const WorkflowDetailsAttributes = connect(null, mapDispatchToProps)(
                 <Grid item xs={12} >
                     <DetailsAttribute
                         label='Owner' linkToUuid={workflow?.ownerUuid}
-                        uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+                        uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
                 </Grid>
                 <Grid item xs={12}>
                     <DetailsAttribute label='Created at' value={formatDate(workflow?.createdAt)} />
@@ -72,7 +71,7 @@ export const WorkflowDetailsAttributes = connect(null, mapDispatchToProps)(
                 <Grid item xs={12} >
                     <DetailsAttribute
                         label='Last modified by user' linkToUuid={workflow?.modifiedByUserUuid}
-                        uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+                        uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
                 </Grid>
             </Grid >;
         }));
index dce8ef8f68cde0083a14703aee13e05f966f339e..9d127a605cc617cd2c7367aa460a039c185e34e9 100644 (file)
@@ -21,7 +21,7 @@ import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, CollectionOldVersionIcon
 import { DetailsAttribute } from 'components/details-attribute/details-attribute';
 import { CollectionResource, getCollectionUrl } from 'models/collection';
 import { CollectionPanelFiles } from 'views-components/collection-panel-files/collection-panel-files';
-import { navigateToProcess, collectionPanelActions } from 'store/collection-panel/collection-panel-action';
+import { navigateToProcess } from 'store/collection-panel/collection-panel-action';
 import { getResource } from 'store/resources/resources';
 import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
 import { formatDate, formatFileSize } from "common/formatters";
@@ -32,11 +32,9 @@ import { IllegalNamingWarning } from 'components/warning/warning';
 import { GroupResource } from 'models/group';
 import { UserResource } from 'models/user';
 import { getUserUuid } from 'common/getuser';
-import { getProgressIndicator } from 'store/progress-indicator/progress-indicator-reducer';
-import { COLLECTION_PANEL_LOAD_FILES, loadCollectionFiles, COLLECTION_PANEL_LOAD_FILES_THRESHOLD } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions';
 import { Link } from 'react-router-dom';
 import { Link as ButtonLink } from '@material-ui/core';
-import { ResourceOwnerWithName, ResponsiblePerson } from 'views-components/data-explorer/renderers';
+import { ResourceWithName, ResponsiblePerson } from 'views-components/data-explorer/renderers';
 import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
 
 type CssRules = 'root'
@@ -115,14 +113,12 @@ interface CollectionPanelDataProps {
     isWritable: boolean;
     isOldVersion: boolean;
     isLoadingFiles: boolean;
-    tooManyFiles: boolean;
 }
 
-type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
-    & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+type CollectionPanelProps = CollectionPanelDataProps & DispatchProp & WithStyles<CssRules>
 
-export const CollectionPanel = withStyles(styles)(
-    connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
+export const CollectionPanel = withStyles(styles)(connect(
+    (state: RootState, props: RouteComponentProps<{ id: string }>) => {
         const currentUserUUID = getUserUuid(state);
         const item = getResource<CollectionResource>(props.match.params.id)(state.resources);
         let isWritable = false;
@@ -137,17 +133,14 @@ export const CollectionPanel = withStyles(styles)(
                 }
             }
         }
-        const loadingFilesIndicator = getProgressIndicator(COLLECTION_PANEL_LOAD_FILES)(state.progressIndicator);
-        const isLoadingFiles = (loadingFilesIndicator && loadingFilesIndicator!.working) || false;
-        const tooManyFiles = (!state.collectionPanel.loadBigCollections && item && item.fileCount > COLLECTION_PANEL_LOAD_FILES_THRESHOLD) || false;
-        return { item, isWritable, isOldVersion, isLoadingFiles, tooManyFiles };
+        return { item, isWritable, isOldVersion };
     })(
         class extends React.Component<CollectionPanelProps> {
             render() {
-                const { classes, item, dispatch, isWritable, isOldVersion, isLoadingFiles, tooManyFiles } = this.props;
+                const { classes, item, dispatch, isWritable, isOldVersion } = this.props;
                 const panelsData: MPVPanelState[] = [
-                    {name: "Details"},
-                    {name: "Files"},
+                    { name: "Details" },
+                    { name: "Files" },
                 ];
                 return item
                     ? <MPVContainer className={classes.root} spacing={8} direction="column" justify-content="flex-start" wrap="nowrap" panelStates={panelsData}>
@@ -195,7 +188,7 @@ export const CollectionPanel = withStyles(styles)(
                                         {isOldVersion &&
                                             <Typography className={classes.warningLabel} variant="caption">
                                                 This is an old version. Make a copy to make changes. Go to the <Link to={getCollectionUrl(item.currentVersionUuid)}>head version</Link> for sharing options.
-                                          </Typography>
+                                            </Typography>
                                         }
                                     </Grid>
                                 </Grid>
@@ -203,15 +196,7 @@ export const CollectionPanel = withStyles(styles)(
                         </MPVPanelContent>
                         <MPVPanelContent xs>
                             <Card className={classes.filesCard}>
-                                <CollectionPanelFiles
-                                    isWritable={isWritable}
-                                    isLoading={isLoadingFiles}
-                                    tooManyFiles={tooManyFiles}
-                                    loadFilesFunc={() => {
-                                        dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true));
-                                        dispatch<any>(loadCollectionFiles(this.props.item.uuid));
-                                    }
-                                    } />
+                                <CollectionPanelFiles isWritable={isWritable} />
                             </Card>
                         </MPVPanelContent>
                     </MPVContainer>
@@ -288,7 +273,7 @@ export const CollectionDetailsAttributes = (props: CollectionDetailsProps) => {
         <Grid item xs={12} md={mdSize}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
                 label='Owner' linkToUuid={item.ownerUuid}
-                uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+                uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
         </Grid>
         <div data-cy="responsible-person-wrapper" ref={responsiblePersonRef}>
             <Grid item xs={12} md={12}>
@@ -341,13 +326,13 @@ export const CollectionDetailsAttributes = (props: CollectionDetailsProps) => {
         <Grid item xs={12} md={12}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
                 label='Properties' />
-            { Object.keys(item.properties).length > 0
+            {Object.keys(item.properties).length > 0
                 ? Object.keys(item.properties).map(k =>
-                        Array.isArray(item.properties[k])
+                    Array.isArray(item.properties[k])
                         ? item.properties[k].map((v: string) =>
                             getPropertyChip(k, v, undefined, classes.tag))
                         : getPropertyChip(k, item.properties[k], undefined, classes.tag))
-                : <div className={classes.value}>No properties</div> }
+                : <div className={classes.value}>No properties</div>}
         </Grid>
     </Grid>;
 };
index 99a4404c87e90fbca886d39b872415857c06c007..1e3e5591e78341a4253ba2eb2442537b9923c70e 100644 (file)
@@ -9,7 +9,7 @@ import { formatDate } from "common/formatters";
 import { resourceLabel } from "common/labels";
 import { DetailsAttribute } from "components/details-attribute/details-attribute";
 import { ResourceKind } from "models/resource";
-import { ContainerRunTime, ResourceOwnerWithName } from "views-components/data-explorer/renderers";
+import { ContainerRunTime, ResourceWithName } from "views-components/data-explorer/renderers";
 import { getProcess, getProcessStatus } from "store/processes/process";
 import { RootState } from "store/store";
 import { connect } from "react-redux";
@@ -79,7 +79,7 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
                 <Grid item xs={12} md={mdSize}>
                     <DetailsAttribute
                         label='Owner' linkToUuid={containerRequest.ownerUuid}
-                        uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+                        uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
                 </Grid>
                 <Grid item xs={12} md={mdSize}>
                     <DetailsAttribute label='Container UUID' value={containerRequest.containerUuid} />
index e0fcb48cba2c6e2c1c9adbb80f78b56f3f124bcf..ccb40d53ba958b35645c14fb0af4a874e72f1b88 100644 (file)
@@ -180,7 +180,7 @@ export const ProjectPanel = withStyles(styles)(
                         name: resource.name,
                         uuid: resource.uuid,
                         ownerUuid: resource.ownerUuid,
-                        isTrashed: ('isTrashed' in resource) ? resource.isTrashed: false,
+                        isTrashed: ('isTrashed' in resource) ? resource.isTrashed : false,
                         kind: resource.kind,
                         menuKind,
                         description: resource.description,