Merge branch '17018-readonly-file-actions-fix'
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 5 Nov 2020 22:51:28 +0000 (19:51 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 5 Nov 2020 22:51:28 +0000 (19:51 -0300)
Closes #17018

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

23 files changed:
.licenseignore
cypress/integration/collection-panel.spec.js
cypress/integration/favorites.js
cypress/integration/login.spec.js
cypress/integration/search.spec.js [new file with mode: 0644]
cypress/support/commands.js
src/common/formatters.ts
src/components/details-attribute/details-attribute.tsx
src/components/file-tree/file-tree-item.tsx
src/components/icon/icon.tsx
src/index.tsx
src/store/context-menu/context-menu-actions.test.ts
src/store/context-menu/context-menu-actions.ts
src/store/open-in-new-tab/open-in-new-tab.actions.ts
src/views-components/context-menu/action-sets/collection-action-set.ts
src/views-components/context-menu/action-sets/collection-admin-action-set.ts
src/views-components/context-menu/action-sets/collection-resource-action-set.ts [deleted file]
src/views-components/context-menu/context-menu.tsx
src/views-components/details-panel/collection-details.tsx
src/views-components/search-bar/search-bar-view.tsx
src/views/collection-panel/collection-panel.tsx
src/views/search-results-panel/search-results-panel-view.tsx
tools/arvados_config.yml

index 7ac3c8369abb351a31acb5ec6a4d616b4fb9efc1..853135fc885f09ad99bfec03cfc12ec0190560f6 100644 (file)
@@ -13,3 +13,4 @@ public/*
 .yarnrc
 .npmrc
 src/lib/cwl-svg/*
+tools/arvados_config.yml
index 47281868ca02d4ab5a11a92d663558828d58f272..404d1c5b04f7ec2b2a17729908b0e6f0130dc60e 100644 (file)
@@ -54,7 +54,8 @@ describe('Collection panel tests', function() {
                     // Check that name & uuid are correct.
                     cy.get('[data-cy=collection-info-panel]')
                         .should('contain', this.testCollection.name)
-                        .and('contain', this.testCollection.uuid);
+                        .and('contain', this.testCollection.uuid)
+                        .and('not.contain', 'This is an old version');
                     // Check for the read-only icon
                     cy.get('[data-cy=read-only-icon]').should(`${isWritable ? 'not.' : ''}exist`);
                     // Check that both read and write operations are available on
@@ -126,4 +127,54 @@ describe('Collection panel tests', function() {
             })
         })
     })
+
+    it('can correctly display old versions', function() {
+        const colName = `Versioned Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+        let colUuid = '';
+        let oldVersionUuid = '';
+        // Make sure no other collections with this name exist
+        cy.doRequest('GET', '/arvados/v1/collections', null, {
+            filters: `[["name", "=", "${colName}"]]`,
+            include_old_versions: true
+        })
+        .its('body.items').as('collections')
+        .then(function() {
+            expect(this.collections).to.be.empty;
+        });
+        // Creates the collection using the admin token so we can set up
+        // a bogus manifest text without block signatures.
+        cy.createCollection(adminUser.token, {
+            name: colName,
+            owner_uuid: activeUser.user.uuid,
+            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
+        .as('originalVersion').then(function() {
+            // Change the file name to create a new version.
+            cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
+                manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n"
+            })
+            colUuid = this.originalVersion.uuid;
+        });
+        // Confirm that there are 2 versions of the collection
+        cy.doRequest('GET', '/arvados/v1/collections', null, {
+            filters: `[["name", "=", "${colName}"]]`,
+            include_old_versions: true
+        })
+        .its('body.items').as('collections')
+        .then(function() {
+            expect(this.collections).to.have.lengthOf(2);
+            this.collections.map(function(aCollection) {
+                expect(aCollection.current_version_uuid).to.equal(colUuid);
+                if (aCollection.uuid !== aCollection.current_version_uuid) {
+                    oldVersionUuid = aCollection.uuid;
+                }
+            });
+            // Check the old version displays as what it is.
+            cy.loginAs(activeUser)
+            cy.visit(`/collections/${oldVersionUuid}`);
+            cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
+            cy.get('[data-cy=read-only-icon]').should('exist');
+            cy.get('[data-cy=collection-info-panel]').should('contain', colName);
+            cy.get('[data-cy=collection-files-panel]').should('contain', 'bar');
+        });
+    });
 })
index b38399be506b65aba126c700183401601a73b151..0855c94e4ddc1dd12d5eb466a3ce01dc76dd2e92 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-describe('Collection panel tests', function() {
+describe('Favorites tests', function() {
     let activeUser;
     let adminUser;
 
index d88c7a6cfa7252a0825de0ba357a87c418947c14..25c8cd4b8a4179cf9f60ad2e7905e8204fac4788 100644 (file)
@@ -93,7 +93,7 @@ describe('Login tests', function() {
             })
         }, null, activeUser.token, true);
         // Should log the user out.
-        cy.get('[data-cy=breadcrumb-first]').click();
+        cy.visit('/');
         cy.get('div#root').should('contain', 'Please log in');
     })
 
diff --git a/cypress/integration/search.spec.js b/cypress/integration/search.spec.js
new file mode 100644 (file)
index 0000000..0fba64c
--- /dev/null
@@ -0,0 +1,83 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('Search tests', function() {
+    let activeUser;
+    let adminUser;
+
+    before(function() {
+        // Only set up common users once. These aren't set up as aliases because
+        // aliases are cleaned up after every test. Also it doesn't make sense
+        // to set the same users on beforeEach() over and over again, so we
+        // separate a little from Cypress' 'Best Practices' here.
+        cy.getUser('admin', 'Admin', 'User', true, true)
+            .as('adminUser').then(function() {
+                adminUser = this.adminUser;
+            }
+        );
+        cy.getUser('collectionuser1', 'Collection', 'User', false, true)
+            .as('activeUser').then(function() {
+                activeUser = this.activeUser;
+            }
+        );
+    })
+
+    beforeEach(function() {
+        cy.clearCookies()
+        cy.clearLocalStorage()
+    })
+
+    it('can search for old collection versions', function() {
+        const colName = `Versioned Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+        let colUuid = '';
+        let oldVersionUuid = '';
+        // Make sure no other collections with this name exist
+        cy.doRequest('GET', '/arvados/v1/collections', null, {
+            filters: `[["name", "=", "${colName}"]]`,
+            include_old_versions: true
+        })
+        .its('body.items').as('collections')
+        .then(function() {
+            expect(this.collections).to.be.empty;
+        });
+        // Creates the collection using the admin token so we can set up
+        // a bogus manifest text without block signatures.
+        cy.createCollection(adminUser.token, {
+            name: colName,
+            owner_uuid: activeUser.user.uuid,
+            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
+        .as('originalVersion').then(function() {
+            // Change the file name to create a new version.
+            cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
+                manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n"
+            })
+            colUuid = this.originalVersion.uuid;
+        });
+        // Confirm that there are 2 versions of the collection
+        cy.doRequest('GET', '/arvados/v1/collections', null, {
+            filters: `[["name", "=", "${colName}"]]`,
+            include_old_versions: true
+        })
+        .its('body.items').as('collections')
+        .then(function() {
+            expect(this.collections).to.have.lengthOf(2);
+            this.collections.map(function(aCollection) {
+                expect(aCollection.current_version_uuid).to.equal(colUuid);
+                if (aCollection.uuid !== aCollection.current_version_uuid) {
+                    oldVersionUuid = aCollection.uuid;
+                }
+            });
+            cy.loginAs(activeUser);
+            const searchQuery = `${colName} type:arvados#collection`;
+            // Search for only collection's current version
+            cy.visit(`/search-results?q=${encodeURIComponent(searchQuery)}`);
+            cy.get('[data-cy=search-results]').should('contain', 'current');
+            cy.get('[data-cy=search-results]').should('not.contain', 'old version');
+            // ...and then, include old versions.
+            cy.visit(`/search-results?q=${encodeURIComponent(searchQuery + ' is:pastVersion')}`);
+            cy.get('[data-cy=search-results]').should('contain', 'current');
+            cy.get('[data-cy=search-results]').should('contain', 'old version');
+        });
+    });
+});
\ No newline at end of file
index fd5139981fc421cf4304548c8c9980841b273710..228e1cabb14e3b68fe43705add938c03cc45bb0d 100644 (file)
@@ -125,6 +125,14 @@ Cypress.Commands.add(
     }
 )
 
+Cypress.Commands.add(
+    "updateCollection", (token, uuid, data) => {
+        return cy.updateResource(token, 'collections', uuid, {
+            collection: JSON.stringify(data)
+        })
+    }
+)
+
 Cypress.Commands.add(
     "createResource", (token, suffix, data) => {
         return cy.doRequest('POST', '/arvados/v1/'+suffix, data, null, token, true)
@@ -145,6 +153,16 @@ Cypress.Commands.add(
     }
 )
 
+Cypress.Commands.add(
+    "updateResource", (token, suffix, uuid, data) => {
+        return cy.doRequest('PUT', '/arvados/v1/'+suffix+'/'+uuid, data, null, token, true)
+        .its('body').as('resource')
+        .then(function() {
+            return this.resource;
+        })
+    }
+)
+
 Cypress.Commands.add(
     "loginAs", (user) => {
         cy.visit(`/token/?api_token=${user.token}`);
index 1386338c900197a2aa2d918f11a02195d6e88729..55fb050738af1d670984b9c6b38cc3f8bafb7ddc 100644 (file)
@@ -28,7 +28,7 @@ export const formatFileSize = (size?: number) => {
             }
         }
     }
-    return "";
+    return "0 B";
 };
 
 export const formatTime = (time: number, seconds?: boolean) => {
index 8f4708588d786d1d268c0aa70fcd013571732d00..4b8ee8378fa32c8fc3db839629780b2ebc133545 100644 (file)
@@ -20,25 +20,21 @@ type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link' | 'c
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     attribute: {
-        display: 'flex',
-        alignItems: 'flex-start',
-        marginBottom: theme.spacing.unit
+        marginBottom: ".6 rem"
     },
     label: {
         boxSizing: 'border-box',
         color: theme.palette.grey["500"],
-        width: '40%'
+        width: '100%'
     },
     value: {
         boxSizing: 'border-box',
-        width: '60%',
         alignItems: 'flex-start'
     },
     lowercaseValue: {
         textTransform: 'lowercase'
     },
     link: {
-        width: '60%',
         color: theme.palette.primary.main,
         textDecoration: 'none',
         overflowWrap: 'break-word',
@@ -46,9 +42,12 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     copyIcon: {
         marginLeft: theme.spacing.unit,
-        fontSize: '1.125rem',
         color: theme.palette.grey["500"],
-        cursor: 'pointer'
+        cursor: 'pointer',
+        display: 'inline',
+        '& svg': {
+            fontSize: '1rem'
+        }
     }
 });
 
@@ -102,17 +101,19 @@ export const DetailsAttribute = connect(mapStateToProps)(withStyles(styles)(
                 valueNode = value;
             }
             return <Typography component="div" className={classes.attribute}>
-                <Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
+                <Typography component="div" className={classnames([classes.label, classLabel])}>{label}</Typography>
                 <Typography
                     onClick={onValueClick}
-                    component="span"
+                    component="div"
                     className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
                     {valueNode}
                     {children}
                     {linkToUuid && <Tooltip title="Copy">
-                        <CopyToClipboard text={linkToUuid || ""} onCopy={() => this.onCopy("Copied")}>
-                            <CopyIcon className={classes.copyIcon} />
-                        </CopyToClipboard>
+                        <span className={classes.copyIcon}>
+                            <CopyToClipboard text={linkToUuid || ""} onCopy={() => this.onCopy("Copied")}>
+                                <CopyIcon />
+                            </CopyToClipboard>
+                        </span>
                     </Tooltip>}
                 </Typography>
             </Typography>;
index 23273daceb3e037538c9707ee676a830a69de833..6f5ab83d8acf062b66fac0117f90b05a4013df77 100644 (file)
@@ -4,7 +4,7 @@
 
 import * as React from "react";
 import { TreeItem } from "../tree/tree";
-import { ProjectIcon, MoreOptionsIcon, DefaultIcon, CollectionIcon } from "../icon/icon";
+import { DirectoryIcon, MoreOptionsIcon, DefaultIcon, FileIcon } from "../icon/icon";
 import { Typography, IconButton, StyleRulesCallback, withStyles, WithStyles, Tooltip } from '@material-ui/core';
 import { formatFileSize } from "~/common/formatters";
 import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon";
@@ -71,9 +71,9 @@ export const FileTreeItem = withStyles(fileTreeItemStyle)(
 export const getIcon = (type: string) => {
     switch (type) {
         case 'directory':
-            return ProjectIcon;
+            return DirectoryIcon;
         case 'file':
-            return CollectionIcon;
+            return FileIcon;
         default:
             return DefaultIcon;
     }
index 2573d764d5f79207141db7887caf0edcb1f8561e..55c3c5a50f44759f10bbee681299b5ebe92cc32a 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { Badge, Tooltip } from '@material-ui/core';
 import Add from '@material-ui/icons/Add';
 import ArrowBack from '@material-ui/icons/ArrowBack';
 import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
@@ -27,6 +28,7 @@ import Folder from '@material-ui/icons/Folder';
 import GetApp from '@material-ui/icons/GetApp';
 import Help from '@material-ui/icons/Help';
 import HelpOutline from '@material-ui/icons/HelpOutline';
+import History from '@material-ui/icons/History';
 import Inbox from '@material-ui/icons/Inbox';
 import Info from '@material-ui/icons/Info';
 import Input from '@material-ui/icons/Input';
@@ -73,6 +75,13 @@ export const ReadOnlyIcon = (props:any) =>
         </div>
     </span>;
 
+export const CollectionOldVersionIcon = (props: any) =>
+    <Tooltip title='Old version'>
+        <Badge badgeContent={<History fontSize='small' />}>
+            <CollectionIcon {...props} />
+        </Badge>
+    </Tooltip>;
+
 export type IconType = React.SFC<{ className?: string, style?: object }>;
 
 export const AddIcon: IconType = (props) => <Add {...props} />;
@@ -89,11 +98,13 @@ export const CloseIcon: IconType = (props) => <Close {...props} />;
 export const CloudUploadIcon: IconType = (props) => <CloudUpload {...props} />;
 export const DefaultIcon: IconType = (props) => <RateReview {...props} />;
 export const DetailsIcon: IconType = (props) => <Info {...props} />;
+export const DirectoryIcon: IconType = (props) => <Folder {...props} />;
 export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
 export const EditSavedQueryIcon: IconType = (props) => <Create {...props} />;
 export const ExpandIcon: IconType = (props) => <ExpandMoreIcon {...props} />;
 export const ErrorIcon: IconType = (props) => <ErrorRoundedIcon style={{color: '#ff0000'}} {...props} />;
 export const FavoriteIcon: IconType = (props) => <Star {...props} />;
+export const FileIcon: IconType = (props) => <LibraryBooks {...props} />;
 export const HelpIcon: IconType = (props) => <Help {...props} />;
 export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
 export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
index 92a2716b6a7fbe6c24010e68994cfc7295680f51..569656d9117874646b238616330e25d640aa932e 100644 (file)
@@ -28,7 +28,6 @@ import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from '~/vi
 import { collectionFilesItemActionSet, readOnlyCollectionFilesItemActionSet } from '~/views-components/context-menu/action-sets/collection-files-item-action-set';
 import { collectionFilesNotSelectedActionSet } from '~/views-components/context-menu/action-sets/collection-files-not-selected-action-set';
 import { collectionActionSet, readOnlyCollectionActionSet } from '~/views-components/context-menu/action-sets/collection-action-set';
-import { collectionResourceActionSet } from '~/views-components/context-menu/action-sets/collection-resource-action-set';
 import { processActionSet } from '~/views-components/context-menu/action-sets/process-action-set';
 import { loadWorkbench } from '~/store/workbench/workbench-actions';
 import { Routes } from '~/routes/routes';
@@ -78,7 +77,6 @@ addMenuActionSet(ContextMenuKind.COLLECTION_FILES_NOT_SELECTED, collectionFilesN
 addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActionSet);
 addMenuActionSet(ContextMenuKind.READONLY_COLLECTION_FILES_ITEM, readOnlyCollectionFilesItemActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
-addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
 addMenuActionSet(ContextMenuKind.READONLY_COLLECTION, readOnlyCollectionActionSet);
 addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
index 4bcbf9f2bb59736c0e32f609e8d02e1f471e77ef..c3e78679278f69783ff5c7a055da5def03217729 100644 (file)
@@ -71,7 +71,7 @@ describe('context-menu-actions', () => {
                 expect(result).toEqual(ContextMenuKind.COLLECTION_ADMIN);
             });
 
-            it('should return ContextMenuKind.COLLECTION_RESOURCE', () => {
+            it('should return ContextMenuKind.COLLECTION', () => {
                 // given
                 const isAdmin = false;
                 const isEditable = true;
@@ -80,7 +80,7 @@ describe('context-menu-actions', () => {
                 const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
 
                 // then
-                expect(result).toEqual(ContextMenuKind.COLLECTION_RESOURCE);
+                expect(result).toEqual(ContextMenuKind.COLLECTION);
             });
 
             it('should return ContextMenuKind.READONLY_COLLECTION', () => {
index 1f766bd3e4ad38f0cf50fc934e1982fa9ec6782e..308d5e88134520296c0088ffef9c662755647a71 100644 (file)
@@ -204,15 +204,21 @@ export const resourceKindToContextMenuKind = (uuid: string, isAdmin?: boolean, i
     const kind = extractUuidKind(uuid);
     switch (kind) {
         case ResourceKind.PROJECT:
-            return !isAdmin ?
-                isEditable ? ContextMenuKind.PROJECT : ContextMenuKind.READONLY_PROJECT :
-                ContextMenuKind.PROJECT_ADMIN;
+            return !isAdmin
+                ? isEditable
+                    ? ContextMenuKind.PROJECT
+                    : ContextMenuKind.READONLY_PROJECT
+                : ContextMenuKind.PROJECT_ADMIN;
         case ResourceKind.COLLECTION:
-            return !isAdmin ?
-                isEditable ? ContextMenuKind.COLLECTION_RESOURCE : ContextMenuKind.READONLY_COLLECTION :
-                ContextMenuKind.COLLECTION_ADMIN;
+            return !isAdmin
+                ? isEditable
+                    ? ContextMenuKind.COLLECTION
+                    : ContextMenuKind.READONLY_COLLECTION
+                : ContextMenuKind.COLLECTION_ADMIN;
         case ResourceKind.PROCESS:
-            return !isAdmin ? ContextMenuKind.PROCESS_RESOURCE : ContextMenuKind.PROCESS_ADMIN;
+            return !isAdmin
+                ? ContextMenuKind.PROCESS_RESOURCE
+                : ContextMenuKind.PROCESS_ADMIN;
         case ResourceKind.USER:
             return ContextMenuKind.ROOT_PROJECT;
         case ResourceKind.LINK:
index 42bdc4ccfe5c25de8a33e277b0d34a4c81f24b98..17ba740279993029b5e5f197c289ef2af33e625f 100644 (file)
@@ -19,10 +19,7 @@ export const openInNewTabAction = (resource: any) => (dispatch: Dispatch) => {
 
     if (kind === ResourceKind.COLLECTION) {
         dispatch(openInNewTabActions.OPEN_COLLECTION_IN_NEW_TAB(uuid));
-    }
-    if (kind === ResourceKind.PROJECT) {
+    } else if (kind === ResourceKind.PROJECT) {
         dispatch(openInNewTabActions.OPEN_PROJECT_IN_NEW_TAB(uuid));
     }
-
-    console.log(uuid);
 };
\ No newline at end of file
index fba2a53acc2d3a42b8400e6ce4fffdbc45c25287..7fa6f2241f5144f6c21eeefef5eb2375d737fd09 100644 (file)
@@ -27,7 +27,7 @@ export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
     },
     {
         icon: CopyIcon,
-        name: "Copy to project",
+        name: "Make a copy",
         execute: (dispatch, resource) => {
             dispatch<any>(openCollectionCopyDialog(resource));
         }
index db849136f6dce0391e93431ef02f72dba5c8d2b4..10a839d842592dc41a1bf3e0745550c83b105663 100644 (file)
@@ -57,7 +57,7 @@ export const collectionAdminActionSet: ContextMenuActionSet = [[
     },
     {
         icon: CopyIcon,
-        name: "Copy to project",
+        name: "Make a copy",
         execute: (dispatch, resource) => {
             dispatch<any>(openCollectionCopyDialog(resource));
         }
@@ -70,13 +70,6 @@ export const collectionAdminActionSet: ContextMenuActionSet = [[
             dispatch<any>(toggleDetailsPanel());
         }
     },
-    // {
-    //     icon: ProvenanceGraphIcon,
-    //     name: "Provenance graph",
-    //     execute: (dispatch, resource) => {
-    //         // add code
-    //     }
-    // },
     {
         icon: AdvancedIcon,
         name: "Advanced",
@@ -90,11 +83,4 @@ export const collectionAdminActionSet: ContextMenuActionSet = [[
             dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
         }
     },
-    // {
-    //     icon: RemoveIcon,
-    //     name: "Remove",
-    //     execute: (dispatch, resource) => {
-    //         // add code
-    //     }
-    // }
 ]];
diff --git a/src/views-components/context-menu/action-sets/collection-resource-action-set.ts b/src/views-components/context-menu/action-sets/collection-resource-action-set.ts
deleted file mode 100644 (file)
index 5e36790..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ContextMenuActionSet } from "../context-menu-action-set";
-import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
-import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon } from '~/components/icon/icon';
-import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
-import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
-import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
-import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
-import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
-import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
-import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
-import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
-
-export const collectionResourceActionSet: ContextMenuActionSet = [[
-    {
-        icon: RenameIcon,
-        name: "Edit collection",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openCollectionUpdateDialog(resource));
-        }
-    },
-    {
-        icon: ShareIcon,
-        name: "Share",
-        execute: (dispatch, { uuid }) => {
-            dispatch<any>(openSharingDialog(uuid));
-        }
-    },
-    {
-        component: ToggleFavoriteAction,
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
-    {
-        icon: MoveToIcon,
-        name: "Move to",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openMoveCollectionDialog(resource));
-        }
-    },
-    {
-        icon: CopyIcon,
-        name: "Copy to project",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openCollectionCopyDialog(resource));
-        }
-    },
-    {
-        icon: DetailsIcon,
-        name: "View details",
-        execute: dispatch => {
-            dispatch<any>(toggleDetailsPanel());
-        }
-    },
-    {
-        icon: AdvancedIcon,
-        name: "Advanced",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openAdvancedTabDialog(resource.uuid));
-        }
-    },
-    {
-        component: ToggleTrashAction,
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
-        }
-    },
-    // {
-    //     icon: RemoveIcon,
-    //     name: "Remove",
-    //     execute: (dispatch, resource) => {
-    //         // add code
-    //     }
-    // }
-]];
index db5765ee1281093c983989f45432aee38d2c8434..43474dd1942e5bc7ea14fb5993a909e584731048 100644 (file)
@@ -77,7 +77,6 @@ export enum ContextMenuKind {
     COLLECTION_FILES_NOT_SELECTED = "CollectionFilesNotSelected",
     COLLECTION = 'Collection',
     COLLECTION_ADMIN = 'CollectionAdmin',
-    COLLECTION_RESOURCE = 'CollectionResource',
     READONLY_COLLECTION = 'ReadOnlyCollection',
     TRASHED_COLLECTION = 'TrashedCollection',
     PROCESS = "Process",
index 999d4c79ffe2d5f240ab69af47debe1ff605ec71..625d8405f0d2f34748b8cd6ffcc4e681fb25c23b 100644 (file)
@@ -5,11 +5,8 @@
 import * as React from 'react';
 import { CollectionIcon } from '~/components/icon/icon';
 import { CollectionResource } from '~/models/collection';
-import { formatDate, formatFileSize } from '~/common/formatters';
-import { resourceLabel } from '~/common/labels';
-import { ResourceKind } from '~/models/resource';
 import { DetailsData } from "./details-data";
-import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
+import { CollectionDetailsAttributes } from '~/views/collection-panel/collection-panel';
 
 export class CollectionDetails extends DetailsData<CollectionResource> {
 
@@ -18,17 +15,6 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
     }
 
     getDetails() {
-        return <div>
-            <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.COLLECTION)} />
-            <DetailsAttribute label='Size' value='---' />
-            <DetailsAttribute label='Owner' linkToUuid={this.item.ownerUuid} lowercaseValue={true} />
-            <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
-            <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
-            <DetailsAttribute label='Collection UUID' linkToUuid={this.item.uuid} value={this.item.uuid} />
-            <DetailsAttribute label='Content address' linkToUuid={this.item.portableDataHash} value={this.item.portableDataHash} />
-            {/* Missing attrs */}
-            <DetailsAttribute label='Number of files' value={this.item.fileCount} />
-            <DetailsAttribute label='Content size' value={formatFileSize(this.item.fileSizeTotal)} />
-        </div>;
+        return <CollectionDetailsAttributes item={this.item} twoCol={false} />;
     }
 }
index 49a8ba6235e5183c2a528287572c2f7081d11dca..20536fd7e1422d2e3771829deb33e897eafed8d2 100644 (file)
@@ -177,6 +177,7 @@ export const SearchBarView = compose(connectVocabulary, withStyles(styles))(
                     <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
                         <form onSubmit={this.handleSubmit}>
                             <Input
+                                data-cy='search-input-field'
                                 className={classes.input}
                                 onChange={this.handleChange}
                                 placeholder="Search"
index 953e5b4c7d281ea6f817fba5b7d98ae545150592..4cdd8c55f0d231ed5925a147ff94ba6482b94145 100644 (file)
@@ -12,16 +12,16 @@ import { connect, DispatchProp } from "react-redux";
 import { RouteComponentProps } from 'react-router';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { RootState } from '~/store/store';
-import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, ExpandIcon } from '~/components/icon/icon';
+import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, ExpandIcon, CollectionOldVersionIcon } from '~/components/icon/icon';
 import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
-import { CollectionResource } from '~/models/collection';
+import { CollectionResource, getCollectionUrl } from '~/models/collection';
 import { CollectionPanelFiles } from '~/views-components/collection-panel-files/collection-panel-files';
 import { CollectionTagForm } from './collection-tag-form';
 import { deleteCollectionTag, navigateToProcess, collectionPanelActions } from '~/store/collection-panel/collection-panel-action';
 import { getResource } from '~/store/resources/resources';
 import { openContextMenu } from '~/store/context-menu/context-menu-actions';
 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-import { formatFileSize } from "~/common/formatters";
+import { formatDate, formatFileSize } from "~/common/formatters";
 import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { getPropertyChip } from '~/views-components/resource-properties-form/property-chip';
@@ -31,6 +31,7 @@ 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';
 
 type CssRules = 'root'
     | 'filesCard'
@@ -40,6 +41,8 @@ type CssRules = 'root'
     | 'value'
     | 'link'
     | 'centeredLabel'
+    | 'warningLabel'
+    | 'collectionName'
     | 'readOnlyIcon';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
@@ -67,6 +70,12 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         fontSize: '0.875rem',
         textAlign: 'center'
     },
+    warningLabel: {
+        fontStyle: 'italic'
+    },
+    collectionName: {
+        flexDirection: 'column',
+    },
     value: {
         textTransform: 'none',
         fontSize: '0.875rem'
@@ -87,6 +96,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 interface CollectionPanelDataProps {
     item: CollectionResource;
     isWritable: boolean;
+    isOldVersion: boolean;
     isLoadingFiles: boolean;
     tooManyFiles: boolean;
 }
@@ -99,72 +109,75 @@ export const CollectionPanel = withStyles(styles)(
         const currentUserUUID = getUserUuid(state);
         const item = getResource<CollectionResource>(props.match.params.id)(state.resources);
         let isWritable = false;
-        if (item && item.ownerUuid === currentUserUUID) {
-            isWritable = true;
-        } else if (item) {
-            const itemOwner = getResource<GroupResource|UserResource>(item.ownerUuid)(state.resources);
-            if (itemOwner) {
-                isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0;
+        const isOldVersion = item && item.currentVersionUuid !== item.uuid;
+        if (item && !isOldVersion) {
+            if (item.ownerUuid === currentUserUUID) {
+                isWritable = true;
+            } else {
+                const itemOwner = getResource<GroupResource | UserResource>(item.ownerUuid)(state.resources);
+                if (itemOwner) {
+                    isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0;
+                }
             }
         }
         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, isLoadingFiles, tooManyFiles };
+        return { item, isWritable, isOldVersion, isLoadingFiles, tooManyFiles };
     })(
         class extends React.Component<CollectionPanelProps> {
             render() {
-                const { classes, item, dispatch, isWritable, isLoadingFiles, tooManyFiles } = this.props;
+                const { classes, item, dispatch, isWritable, isOldVersion, isLoadingFiles, tooManyFiles } = this.props;
                 return item
                     ? <div className={classes.root}>
                         <ExpansionPanel data-cy='collection-info-panel' defaultExpanded>
                             <ExpansionPanelSummary expandIcon={<ExpandIcon />}>
-                                <span>
-                                    <IconButton onClick={this.openCollectionDetails}>
-                                        <CollectionIcon className={classes.iconHeader} />
-                                    </IconButton>
-                                    <IllegalNamingWarning name={item.name}/>
-                                    {item.name}
-                                    {isWritable ||
-                                    <Tooltip title="Read-only">
-                                        <ReadOnlyIcon data-cy="read-only-icon" className={classes.readOnlyIcon} />
-                                    </Tooltip>
-                                    }
-                                </span>
+                                <Grid container justify="space-between">
+                                    <Grid item xs={11}><span>
+                                        <IconButton onClick={this.openCollectionDetails}>
+                                            {isOldVersion
+                                                ? <CollectionOldVersionIcon className={classes.iconHeader} />
+                                                : <CollectionIcon className={classes.iconHeader} />}
+                                        </IconButton>
+                                        <IllegalNamingWarning name={item.name} />
+                                        <span>
+                                            {item.name}
+                                            {isWritable ||
+                                                <Tooltip title="Read-only">
+                                                    <ReadOnlyIcon data-cy="read-only-icon" className={classes.readOnlyIcon} />
+                                                </Tooltip>
+                                            }
+                                        </span>
+                                    </span></Grid>
+                                    <Grid item xs={1} style={{ textAlign: "right" }}>
+                                        <Tooltip title="Actions" disableFocusListener>
+                                            <IconButton
+                                                data-cy='collection-panel-options-btn'
+                                                aria-label="Actions"
+                                                onClick={this.handleContextMenu}>
+                                                <MoreOptionsIcon />
+                                            </IconButton>
+                                        </Tooltip>
+                                    </Grid>
+                                </Grid>
                             </ExpansionPanelSummary>
                             <ExpansionPanelDetails>
                                 <Grid container justify="space-between">
-                                    <Grid item xs={11}>
+                                    <Grid item xs={12}>
                                         <Typography variant="caption">
                                             {item.description}
                                         </Typography>
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                            label='Collection UUID'
-                                            linkToUuid={item.uuid} />
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                            label='Portable data hash'
-                                            linkToUuid={item.portableDataHash} />
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                            label='Number of files' value={item.fileCount} />
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                            label='Content size' value={formatFileSize(item.fileSizeTotal)} />
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                            label='Owner' linkToUuid={item.ownerUuid} />
+                                        <CollectionDetailsAttributes item={item} classes={classes} twoCol={true} />
                                         {(item.properties.container_request || item.properties.containerRequest) &&
                                             <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
                                                 <DetailsAttribute classLabel={classes.link} label='Link to process' />
                                             </span>
                                         }
-                                    </Grid>
-                                    <Grid item xs={1} style={{textAlign: "right"}}>
-                                        <Tooltip title="More options" disableFocusListener>
-                                            <IconButton
-                                                data-cy='collection-panel-options-btn'
-                                                aria-label="More options"
-                                                onClick={this.handleContextMenu}>
-                                                <MoreOptionsIcon />
-                                            </IconButton>
-                                        </Tooltip>
+                                        {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>
+                                        }
                                     </Grid>
                                 </Grid>
                             </ExpansionPanelDetails>
@@ -180,25 +193,25 @@ export const CollectionPanel = withStyles(styles)(
                                         <CollectionTagForm />
                                     </Grid>}
                                     <Grid item xs={12}>
-                                    Object.keys(item.properties).length > 0
-                                        ? Object.keys(item.properties).map(k =>
-                                            Array.isArray(item.properties[k])
-                                            ? item.properties[k].map((v: string) =>
-                                                getPropertyChip(
-                                                    k, v,
-                                                    isWritable
-                                                        ? this.handleDelete(k, item.properties[k])
-                                                        : undefined,
-                                                    classes.tag))
-                                            : getPropertyChip(
-                                                k, item.properties[k],
-                                                isWritable
-                                                    ? this.handleDelete(k, item.properties[k])
-                                                    : undefined,
-                                                classes.tag)
-                                        )
-                                        : <div className={classes.centeredLabel}>No properties set on this collection.</div>
-                                    }
+                                        {Object.keys(item.properties).length > 0
+                                            ? Object.keys(item.properties).map(k =>
+                                                Array.isArray(item.properties[k])
+                                                    ? item.properties[k].map((v: string) =>
+                                                        getPropertyChip(
+                                                            k, v,
+                                                            isWritable
+                                                                ? this.handleDelete(k, item.properties[k])
+                                                                : undefined,
+                                                            classes.tag))
+                                                    : getPropertyChip(
+                                                        k, item.properties[k],
+                                                        isWritable
+                                                            ? this.handleDelete(k, item.properties[k])
+                                                            : undefined,
+                                                        classes.tag)
+                                            )
+                                            : <div className={classes.centeredLabel}>No properties set on this collection.</div>
+                                        }
                                     </Grid>
                                 </Grid>
                             </ExpansionPanelDetails>
@@ -212,7 +225,7 @@ export const CollectionPanel = withStyles(styles)(
                                     dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true));
                                     dispatch<any>(loadCollectionFiles(this.props.item.uuid));
                                 }
-                            } />
+                                } />
                         </div>
                     </div>
                     : null;
@@ -233,6 +246,8 @@ export const CollectionPanel = withStyles(styles)(
                             : ContextMenuKind.COLLECTION
                         : ContextMenuKind.READONLY_COLLECTION
                 };
+                // Avoid expanding/collapsing the panel
+                event.stopPropagation();
                 this.props.dispatch<any>(openContextMenu(event, resource));
             }
 
@@ -261,3 +276,52 @@ export const CollectionPanel = withStyles(styles)(
         }
     )
 );
+
+export const CollectionDetailsAttributes = (props: { item: CollectionResource, twoCol: boolean, classes?: Record<CssRules, string> }) => {
+    const item = props.item;
+    const classes = props.classes || { label: '', value: '' };
+    const isOldVersion = item && item.currentVersionUuid !== item.uuid;
+    const mdSize = props.twoCol ? 6 : 12;
+    return <Grid container>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                label={isOldVersion ? "This version's UUID" : "Collection UUID"}
+                linkToUuid={item.uuid} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                label={isOldVersion ? "This version's PDH" : "Portable data hash"}
+                linkToUuid={item.portableDataHash} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                label='Owner' linkToUuid={item.ownerUuid} />
+        </Grid>
+
+        {isOldVersion &&
+            <Grid item xs={12} md={mdSize}>
+                <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                    label='Head version'
+                    linkToUuid={item.currentVersionUuid} />
+            </Grid>
+        }
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                label='Version number' value={item.version} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Created at' value={formatDate(item.createdAt)} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Last modified' value={formatDate(item.modifiedAt)} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                label='Number of files' value={item.fileCount} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                label='Content size' value={formatFileSize(item.fileSizeTotal)} />
+        </Grid>
+    </Grid>;
+};
index fbaba210e29b94cec6d32def6256c6eba116d342..dae91bd0e5fcb15427857a63e541034dab2d527b 100644 (file)
@@ -108,7 +108,7 @@ export const SearchResultsPanelView = withStyles(styles, { withTheme: true })(
     (props: SearchResultsPanelProps & WithStyles<CssRules, true>) => {
         const homeCluster = props.user.uuid.substr(0, 5);
         const loggedIn = props.sessions.filter((ss) => ss.loggedIn && ss.userIsActive);
-        return <DataExplorer
+        return <span data-cy='search-results'><DataExplorer
             id={SEARCH_RESULTS_PANEL_ID}
             onRowClick={props.onItemClick}
             onRowDoubleClick={props.onItemDoubleClick}
@@ -127,5 +127,5 @@ export const SearchResultsPanelView = withStyles(styles, { withTheme: true })(
                         : <span style={{ marginLeft: "2em" }}>Use <Link to={Routes.SITE_MANAGER} >Site Manager</Link> to manage which clusters will be searched.</span>}
                 </div >
             }
-        />;
+        /></span>;
     });
index 8882eac2e48378ebbe1bdb81b862991995d1611b..b533156de067fb0ac20aec0b63e5bb531b6637e3 100644 (file)
@@ -7,6 +7,8 @@ Clusters:
     TLS:
       Insecure: true
     Collections:
+      CollectionVersioning: true
+      PreserveVersionIfIdle: 0s
       BlobSigningKey: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
       TrustAllContent: true
       ForwardSlashNameSubstitution: /