16719: Merge branch 'master' into 16719-collection-version-basic-ui
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 20 Oct 2020 21:32:23 +0000 (18:32 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 20 Oct 2020 21:32:23 +0000 (18:32 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

19 files changed:
.licenseignore
cypress/integration/collection-panel.spec.js
cypress/integration/search.spec.js [new file with mode: 0644]
cypress/support/commands.js
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/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 c14101d8c15ed94f3f895942797efb7ae5782f14..414d7e3ed40d12e91ba8155e888e4eab700a74ed 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
@@ -117,4 +118,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');
+        });
+    });
 })
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 8f4708588d786d1d268c0aa70fcd013571732d00..18d0d8b797eb8dc6ee168089161712903934bbd0 100644 (file)
@@ -21,8 +21,7 @@ type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link' | 'c
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     attribute: {
         display: 'flex',
-        alignItems: 'flex-start',
-        marginBottom: theme.spacing.unit
+        alignItems: 'flex-start'
     },
     label: {
         boxSizing: 'border-box',
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 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..1089706d5b994c0438d8326b289f84bf8f7245f9 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} />;
     }
 }
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..f41e8967625624a0b801bd4e197c1de0a1cfadaf 100644 (file)
@@ -12,7 +12,7 @@ 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 { CollectionPanelFiles } from '~/views-components/collection-panel-files/collection-panel-files';
@@ -21,7 +21,7 @@ import { deleteCollectionTag, navigateToProcess, collectionPanelActions } from '
 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';
@@ -40,6 +40,8 @@ type CssRules = 'root'
     | 'value'
     | 'link'
     | 'centeredLabel'
+    | 'warningLabel'
+    | 'collectionName'
     | 'readOnlyIcon';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
@@ -67,6 +69,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 +95,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 interface CollectionPanelDataProps {
     item: CollectionResource;
     isWritable: boolean;
+    isOldVersion: boolean;
     isLoadingFiles: boolean;
     tooManyFiles: boolean;
 }
@@ -99,72 +108,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} />
                                         {(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. Copy it as a new one if you need to make changes. Go to the current version if you need to share it.
+                                        </Typography>
+                                        }
                                     </Grid>
                                 </Grid>
                             </ExpansionPanelDetails>
@@ -233,6 +245,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 +275,32 @@ export const CollectionPanel = withStyles(styles)(
         }
     )
 );
+
+export const CollectionDetailsAttributes = (props: {item: CollectionResource, classes?: Record<CssRules, string>}) => {
+    const item = props.item;
+    const classes = props.classes || {label: '', value: ''};
+    const isOldVersion = item && item.currentVersionUuid !== item.uuid;
+    return <span>
+        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+            label={isOldVersion ? "This version's UUID" : "Collection UUID"}
+            linkToUuid={item.uuid} />
+        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+            label={isOldVersion ? "This version's PDH" : "Portable data hash"}
+            linkToUuid={item.portableDataHash} />
+        {isOldVersion &&
+        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+            label='Most recent version'
+            linkToUuid={item.currentVersionUuid} />
+        }
+        <DetailsAttribute label='Last modified' value={formatDate(item.modifiedAt)} />
+        <DetailsAttribute label='Created at' value={formatDate(item.createdAt)} />
+        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+            label='Version number' value={item.version} />
+        <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} />
+    </span>;
+};
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: /