From: Lucas Di Pentima Date: Tue, 2 Jun 2020 19:14:27 +0000 (-0300) Subject: 16439: Merge branch 'master' into 16439-objects-creation-placement-fix X-Git-Tag: 2.1.0~26^2~2 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/6ad3586e61737306f61a330eca545ca494f16304?hp=0cfd92ccb88a7cac4ff6d1645bbbe62386f4bb1b 16439: Merge branch 'master' into 16439-objects-creation-placement-fix Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- diff --git a/cypress.json b/cypress.json index e62577e6..ebe064ea 100644 --- a/cypress.json +++ b/cypress.json @@ -1,4 +1,6 @@ { "baseUrl": "https://localhost:3000/", - "chromeWebSecurity": false + "chromeWebSecurity": false, + "viewportWidth": 1920, + "viewportHeight": 1080 } diff --git a/cypress/integration/collection-panel.spec.js b/cypress/integration/collection-panel.spec.js new file mode 100644 index 00000000..6fc2d565 --- /dev/null +++ b/cypress/integration/collection-panel.spec.js @@ -0,0 +1,120 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +describe('Collection panel 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('shows collection by URL', function() { + cy.loginAs(activeUser); + [true, false].map(function(isWritable) { + cy.createGroup(adminUser.token, { + name: 'Shared project', + group_class: 'project', + }).as('sharedGroup').then(function() { + // Creates the collection using the admin token so we can set up + // a bogus manifest text without block signatures. + cy.createCollection(adminUser.token, { + name: 'Test collection', + owner_uuid: this.sharedGroup.uuid, + properties: {someKey: 'someValue'}, + manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"}) + .as('testCollection').then(function() { + // Share the group with active user. + cy.createLink(adminUser.token, { + name: isWritable ? 'can_write' : 'can_read', + link_class: 'permission', + head_uuid: this.sharedGroup.uuid, + tail_uuid: activeUser.user.uuid + }) + cy.visit(`/collections/${this.testCollection.uuid}`); + // Check that name & uuid are correct. + cy.get('[data-cy=collection-info-panel]') + .should('contain', this.testCollection.name) + .and('contain', this.testCollection.uuid); + // 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 + // the 'More options' menu. + cy.get('[data-cy=collection-panel-options-btn]') + .click() + cy.get('[data-cy=context-menu]') + .should('contain', 'Add to favorites') + .and(`${isWritable ? '' : 'not.'}contain`, 'Edit collection') + .type('{esc}'); // Collapse the options menu + cy.get('[data-cy=collection-properties-panel]') + .should('contain', 'someKey') + .and('contain', 'someValue') + .and('not.contain', 'anotherKey') + .and('not.contain', 'anotherValue') + if (isWritable === true) { + // Check that properties can be added. + cy.get('[data-cy=collection-properties-form]').within(() => { + cy.get('[data-cy=property-field-key]').within(() => { + cy.get('input').type('anotherKey'); + }); + cy.get('[data-cy=property-field-value]').within(() => { + cy.get('input').type('anotherValue'); + }); + cy.root().submit(); + }) + cy.get('[data-cy=collection-properties-panel]') + .should('contain', 'anotherKey') + .and('contain', 'anotherValue') + } else { + // Properties form shouldn't be displayed. + cy.get('[data-cy=collection-properties-form]').should('not.exist'); + } + // Check that the file listing show both read & write operations + cy.get('[data-cy=collection-files-panel]').within(() => { + cy.root().should('contain', 'bar'); + cy.get('[data-cy=upload-button]') + .should(`${isWritable ? '' : 'not.'}contain`, 'Upload data'); + }); + // Hamburger 'more options' menu button + cy.get('[data-cy=collection-files-panel-options-btn]') + .click() + cy.get('[data-cy=context-menu]') + .should('contain', 'Select all') + .click() + cy.get('[data-cy=collection-files-panel-options-btn]') + .click() + cy.get('[data-cy=context-menu]') + .should('contain', 'Download selected') + .and(`${isWritable ? '' : 'not.'}contain`, 'Remove selected') + .type('{esc}'); // Collapse the options menu + // File item 'more options' button + cy.get('[data-cy=file-item-options-btn') + .click() + cy.get('[data-cy=context-menu]') + .should('contain', 'Download') + .and(`${isWritable ? '' : 'not.'}contain`, 'Remove') + .type('{esc}'); // Collapse + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 68ce6870..8baa2db6 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -32,7 +32,7 @@ const controllerURL = Cypress.env('controller_url'); const systemToken = Cypress.env('system_token'); Cypress.Commands.add( - "do_request", (method='GET', path='', data=null, qs=null, + "doRequest", (method='GET', path='', data=null, qs=null, token=systemToken, auth=false, followRedirect=true) => { return cy.request({ method: method, @@ -56,7 +56,7 @@ Cypress.Commands.add( Cypress.Commands.add( "getUser", (username, first_name='', last_name='', is_admin=false, is_active=true) => { // Create user if not already created - return cy.do_request('POST', '/auth/controller/callback', { + return cy.doRequest('POST', '/auth/controller/callback', { auth_info: JSON.stringify({ email: `${username}@example.local`, username: username, @@ -71,13 +71,13 @@ Cypress.Commands.add( .then(function() { this.userToken = this.location.split("=")[1] assert.isString(this.userToken) - return cy.do_request('GET', '/arvados/v1/users', null, { + return cy.doRequest('GET', '/arvados/v1/users', null, { filters: `[["username", "=", "${username}"]]` }) .its('body.items.0') .as('aUser') .then(function() { - cy.do_request('PUT', `/arvados/v1/users/${this.aUser.uuid}`, { + cy.doRequest('PUT', `/arvados/v1/users/${this.aUser.uuid}`, { user: { is_admin: is_admin, is_active: is_active @@ -92,3 +92,48 @@ Cypress.Commands.add( }) } ) + +Cypress.Commands.add( + "createLink", (token, data) => { + return cy.createResource(token, 'links', { + link: JSON.stringify(data) + }) + } +) + +Cypress.Commands.add( + "createGroup", (token, data) => { + return cy.createResource(token, 'groups', { + group: JSON.stringify(data), + ensure_unique_name: true + }) + } +) + +Cypress.Commands.add( + "createCollection", (token, data) => { + return cy.createResource(token, 'collections', { + collection: JSON.stringify(data), + ensure_unique_name: true + }) + } +) + +Cypress.Commands.add( + "createResource", (token, suffix, data) => { + return cy.doRequest('POST', '/arvados/v1/'+suffix, 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}`); + cy.url().should('contain', '/projects/'); + cy.get('div#root').should('contain', 'Arvados Workbench (zzzzz)'); + cy.get('div#root').should('not.contain', 'Your account is inactive'); + } +) \ No newline at end of file diff --git a/package.json b/package.json index 16ad657b..0efdbd7d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.1.0", "private": true, "dependencies": { + "@fortawesome/fontawesome-svg-core": "1.2.28", + "@fortawesome/free-solid-svg-icons": "5.13.0", + "@fortawesome/react-fontawesome": "0.1.9", "@material-ui/core": "3.9.3", "@material-ui/icons": "3.0.1", "@types/debounce": "3.0.0", diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx index 0a443907..48b36be1 100644 --- a/src/components/collection-panel-files/collection-panel-files.tsx +++ b/src/components/collection-panel-files/collection-panel-files.tsx @@ -12,9 +12,10 @@ import { DownloadIcon } from '~/components/icon/icon'; export interface CollectionPanelFilesProps { items: Array>; + isWritable: boolean; onUploadDataClick: () => void; - onItemMenuOpen: (event: React.MouseEvent, item: TreeItem) => void; - onOptionsMenuOpen: (event: React.MouseEvent) => void; + onItemMenuOpen: (event: React.MouseEvent, item: TreeItem, isWritable: boolean) => void; + onOptionsMenuOpen: (event: React.MouseEvent, isWritable: boolean) => void; onSelectionToggle: (event: React.MouseEvent, item: TreeItem) => void; onCollapseToggle: (id: string, status: TreeItemStatus) => void; onFileClick: (id: string) => void; @@ -48,25 +49,30 @@ const styles: StyleRulesCallback = theme => ({ export const CollectionPanelFiles = withStyles(styles)( - ({ onItemMenuOpen, onOptionsMenuOpen, onUploadDataClick, classes, ...treeProps }: CollectionPanelFilesProps & WithStyles) => - + ({ onItemMenuOpen, onOptionsMenuOpen, onUploadDataClick, classes, isWritable, ...treeProps }: CollectionPanelFilesProps & WithStyles) => + Upload data - + } /> - + onOptionsMenuOpen(ev, isWritable)}> @@ -79,5 +85,5 @@ export const CollectionPanelFiles = File size - + onItemMenuOpen(ev, item, isWritable)} {...treeProps} /> ); diff --git a/src/components/context-menu/context-menu.tsx b/src/components/context-menu/context-menu.tsx index 98456dad..ecade812 100644 --- a/src/components/context-menu/context-menu.tsx +++ b/src/components/context-menu/context-menu.tsx @@ -32,7 +32,7 @@ export class ContextMenu extends React.PureComponent { transformOrigin={DefaultTransformOrigin} anchorOrigin={DefaultTransformOrigin} onContextMenu={this.handleContextMenu}> - + {items.map((group, groupIndex) => {group.map((item, actionIndex) => diff --git a/src/components/file-tree/file-tree-item.tsx b/src/components/file-tree/file-tree-item.tsx index dc8f09b9..23273dac 100644 --- a/src/components/file-tree/file-tree-item.tsx +++ b/src/components/file-tree/file-tree-item.tsx @@ -53,6 +53,7 @@ export const FileTreeItem = withStyles(fileTreeItemStyle)( variant="caption">{formatFileSize(item.data.size)} diff --git a/src/components/file-tree/file-tree.tsx b/src/components/file-tree/file-tree.tsx index ad7ac73e..34a11cd6 100644 --- a/src/components/file-tree/file-tree.tsx +++ b/src/components/file-tree/file-tree.tsx @@ -26,7 +26,7 @@ export class FileTree extends React.Component { onContextMenu={this.handleContextMenu} toggleItemActive={this.handleToggleActive} toggleItemOpen={this.handleToggle} - toggleItemSelection={this.handleSelectionChange} + toggleItemSelection={this.handleSelectionChange} currentItemUuid={this.props.currentItemUuid} />; } diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index a3d01e94..b3f44824 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -55,6 +55,23 @@ import StarBorder from '@material-ui/icons/StarBorder'; import Warning from '@material-ui/icons/Warning'; import VpnKey from '@material-ui/icons/VpnKey'; +// Import FontAwesome icons +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faPencilAlt, faSlash } from '@fortawesome/free-solid-svg-icons'; +library.add( + faPencilAlt, + faSlash, +); + +export const ReadOnlyIcon = (props:any) => + +
+ + +
+
; + export type IconType = React.SFC<{ className?: string, style?: object }>; export const AddIcon: IconType = (props) => ; diff --git a/src/index.tsx b/src/index.tsx index d428b1c3..a12dabfa 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -24,10 +24,10 @@ import { rootProjectActionSet } from "~/views-components/context-menu/action-set import { projectActionSet } from "~/views-components/context-menu/action-sets/project-action-set"; import { resourceActionSet } from '~/views-components/context-menu/action-sets/resource-action-set'; import { favoriteActionSet } from "~/views-components/context-menu/action-sets/favorite-action-set"; -import { collectionFilesActionSet } from '~/views-components/context-menu/action-sets/collection-files-action-set'; -import { collectionFilesItemActionSet } from '~/views-components/context-menu/action-sets/collection-files-item-action-set'; +import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from '~/views-components/context-menu/action-sets/collection-files-action-set'; +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 } from '~/views-components/context-menu/action-sets/collection-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'; @@ -70,10 +70,13 @@ addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet); addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet); addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet); addMenuActionSet(ContextMenuKind.COLLECTION_FILES, collectionFilesActionSet); +addMenuActionSet(ContextMenuKind.READONLY_COLLECTION_FILES, readOnlyCollectionFilesActionSet); addMenuActionSet(ContextMenuKind.COLLECTION_FILES_NOT_SELECTED, collectionFilesNotSelectedActionSet); 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); addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet); @@ -101,19 +104,13 @@ fetchConfig() }, errorFn: (id, error) => { console.error("Backend error:", error); - if (error.errors) { - store.dispatch(snackbarActions.OPEN_SNACKBAR({ - message: `${error.errors[0]}`, - kind: SnackbarKind.ERROR, - hideDuration: 8000 - })); - } else { - store.dispatch(snackbarActions.OPEN_SNACKBAR({ - message: `${error.message}`, - kind: SnackbarKind.ERROR, - hideDuration: 8000 - })); - } + store.dispatch(snackbarActions.OPEN_SNACKBAR({ + message: `${error.errors + ? error.errors[0] + : error.message}`, + kind: SnackbarKind.ERROR, + hideDuration: 8000}) + ); } }); const store = configureStore(history, services); diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 431d15e8..2ba6bc2c 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -55,7 +55,7 @@ export const openContextMenu = (event: React.MouseEvent, resource: ); }; -export const openCollectionFilesContextMenu = (event: React.MouseEvent) => +export const openCollectionFilesContextMenu = (event: React.MouseEvent, isWritable: boolean) => (dispatch: Dispatch, getState: () => RootState) => { const isCollectionFileSelected = JSON.stringify(getState().collectionPanelFiles).includes('"selected":true'); dispatch(openContextMenu(event, { @@ -63,7 +63,11 @@ export const openCollectionFilesContextMenu = (event: React.MouseEvent resourcesActions.match(action, { - SET_RESOURCES: resources => resources.reduce((state, resource) => setResource(resource.uuid, resource)(state), state), - DELETE_RESOURCES: ids => ids.reduce((state, id) => deleteResource(id)(state), state), + SET_RESOURCES: resources => resources.reduce( + (state, resource) => setResource(resource.uuid, resource)(state), + state), + DELETE_RESOURCES: ids => ids.reduce( + (state, id) => deleteResource(id)(state), + state), default: () => state, }); \ No newline at end of file diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index 7faad1e8..e2ff01f7 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -283,7 +283,7 @@ export const loadCollection = (uuid: string) => OWNED: async collection => { dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource)); dispatch(updateResources([collection])); - await dispatch(activateSidePanelTreeItem(collection.ownerUuid)); + dispatch(activateSidePanelTreeItem(collection.ownerUuid)); dispatch(setSidePanelBreadcrumbs(collection.ownerUuid)); dispatch(loadCollectionPanel(collection.uuid)); }, diff --git a/src/views-components/collection-panel-files/collection-panel-files.ts b/src/views-components/collection-panel-files/collection-panel-files.ts index e5983b6b..eb16eb6c 100644 --- a/src/views-components/collection-panel-files/collection-panel-files.ts +++ b/src/views-components/collection-panel-files/collection-panel-files.ts @@ -52,11 +52,22 @@ const mapDispatchToProps = (dispatch: Dispatch): Pick { dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: item.id })); }, - onItemMenuOpen: (event, item) => { - dispatch(openContextMenu(event, { menuKind: ContextMenuKind.COLLECTION_FILES_ITEM, kind: ResourceKind.COLLECTION, name: item.data.name, uuid: item.id, ownerUuid: '' })); + onItemMenuOpen: (event, item, isWritable) => { + dispatch(openContextMenu( + event, + { + menuKind: isWritable + ? ContextMenuKind.COLLECTION_FILES_ITEM + : ContextMenuKind.READONLY_COLLECTION_FILES_ITEM, + kind: ResourceKind.COLLECTION, + name: item.data.name, + uuid: item.id, + ownerUuid: '' + } + )); }, - onOptionsMenuOpen: (event) => { - dispatch(openCollectionFilesContextMenu(event)); + onOptionsMenuOpen: (event, isWritable) => { + dispatch(openCollectionFilesContextMenu(event, isWritable)); }, onFileClick: (id) => { dispatch(openDetailsPanel(id)); diff --git a/src/views-components/context-menu/action-sets/collection-action-set.ts b/src/views-components/context-menu/action-sets/collection-action-set.ts index 9629f028..ea97a9b1 100644 --- a/src/views-components/context-menu/action-sets/collection-action-set.ts +++ b/src/views-components/context-menu/action-sets/collection-action-set.ts @@ -16,21 +16,7 @@ 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 collectionActionSet: ContextMenuActionSet = [[ - { - icon: RenameIcon, - name: "Edit collection", - execute: (dispatch, resource) => { - dispatch(openCollectionUpdateDialog(resource)); - } - }, - { - icon: ShareIcon, - name: "Share", - execute: (dispatch, { uuid }) => { - dispatch(openSharingDialog(uuid)); - } - }, +export const readOnlyCollectionActionSet: ContextMenuActionSet = [[ { component: ToggleFavoriteAction, execute: (dispatch, resource) => { @@ -39,11 +25,6 @@ export const collectionActionSet: ContextMenuActionSet = [[ }); } }, - { - icon: MoveToIcon, - name: "Move to", - execute: (dispatch, resource) => dispatch(openMoveCollectionDialog(resource)) - }, { icon: CopyIcon, name: "Copy to project", @@ -59,13 +40,6 @@ export const collectionActionSet: ContextMenuActionSet = [[ dispatch(toggleDetailsPanel()); } }, - // { - // icon: ProvenanceGraphIcon, - // name: "Provenance graph", - // execute: (dispatch, resource) => { - // // add code - // } - // }, { icon: AdvancedIcon, name: "Advanced", @@ -73,17 +47,32 @@ export const collectionActionSet: ContextMenuActionSet = [[ dispatch(openAdvancedTabDialog(resource.uuid)); } }, +]]; + +export const collectionActionSet: ContextMenuActionSet = readOnlyCollectionActionSet.concat([[ + { + icon: RenameIcon, + name: "Edit collection", + execute: (dispatch, resource) => { + dispatch(openCollectionUpdateDialog(resource)); + } + }, + { + icon: ShareIcon, + name: "Share", + execute: (dispatch, { uuid }) => { + dispatch(openSharingDialog(uuid)); + } + }, + { + icon: MoveToIcon, + name: "Move to", + execute: (dispatch, resource) => dispatch(openMoveCollectionDialog(resource)) + }, { component: ToggleTrashAction, execute: (dispatch, resource) => { dispatch(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-files-action-set.ts b/src/views-components/context-menu/action-sets/collection-files-action-set.ts index 885f222c..fc0139c8 100644 --- a/src/views-components/context-menu/action-sets/collection-files-action-set.ts +++ b/src/views-components/context-menu/action-sets/collection-files-action-set.ts @@ -7,32 +7,42 @@ import { collectionPanelFilesAction, openMultipleFilesRemoveDialog } from "~/sto import { openCollectionPartialCopyDialog, openCollectionPartialCopyToSelectedCollectionDialog } from '~/store/collections/collection-partial-copy-actions'; import { DownloadCollectionFileAction } from "~/views-components/context-menu/actions/download-collection-file-action"; -export const collectionFilesActionSet: ContextMenuActionSet = [[{ - name: "Select all", - execute: dispatch => { - dispatch(collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES()); +export const readOnlyCollectionFilesActionSet: ContextMenuActionSet = [[ + { + name: "Select all", + execute: dispatch => { + dispatch(collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES()); + } + }, + { + name: "Unselect all", + execute: dispatch => { + dispatch(collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES()); + } + }, + { + component: DownloadCollectionFileAction, + execute: () => { return; } + }, + { + name: "Create a new collection with selected", + execute: dispatch => { + dispatch(openCollectionPartialCopyDialog()); + } + }, + { + name: "Copy selected into the collection", + execute: dispatch => { + dispatch(openCollectionPartialCopyToSelectedCollectionDialog()); + } } -}, { - name: "Unselect all", - execute: dispatch => { - dispatch(collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES()); - } -}, { - name: "Remove selected", - execute: dispatch => { - dispatch(openMultipleFilesRemoveDialog()); - } -}, { - component: DownloadCollectionFileAction, - execute: () => { return; } -}, { - name: "Create a new collection with selected", - execute: dispatch => { - dispatch(openCollectionPartialCopyDialog()); - } -}, { - name: "Copy selected into the collection", - execute: dispatch => { - dispatch(openCollectionPartialCopyToSelectedCollectionDialog()); - } -}]]; +]]; + +export const collectionFilesActionSet: ContextMenuActionSet = readOnlyCollectionFilesActionSet.concat([[ + { + name: "Remove selected", + execute: dispatch => { + dispatch(openMultipleFilesRemoveDialog()); + } + }, +]]); diff --git a/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts b/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts index 61603edf..4c6874c6 100644 --- a/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts +++ b/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts @@ -9,22 +9,30 @@ import { openFileRemoveDialog, openRenameFileDialog } from '~/store/collection-p import { CollectionFileViewerAction } from '~/views-components/context-menu/actions/collection-file-viewer-action'; -export const collectionFilesItemActionSet: ContextMenuActionSet = [[{ - name: "Rename", - icon: RenameIcon, - execute: (dispatch, resource) => { - dispatch(openRenameFileDialog({ name: resource.name, id: resource.uuid })); +export const readOnlyCollectionFilesItemActionSet: ContextMenuActionSet = [[ + { + component: DownloadCollectionFileAction, + execute: () => { return; } + }, + { + component: CollectionFileViewerAction, + execute: () => { return; }, } -}, { - component: DownloadCollectionFileAction, - execute: () => { return; } -}, { - name: "Remove", - icon: RemoveIcon, - execute: (dispatch, resource) => { - dispatch(openFileRemoveDialog(resource.uuid)); +]]; + +export const collectionFilesItemActionSet: ContextMenuActionSet = readOnlyCollectionFilesItemActionSet.concat([[ + { + name: "Rename", + icon: RenameIcon, + execute: (dispatch, resource) => { + dispatch(openRenameFileDialog({ name: resource.name, id: resource.uuid })); + } + }, + { + name: "Remove", + icon: RemoveIcon, + execute: (dispatch, resource) => { + dispatch(openFileRemoveDialog(resource.uuid)); + } } -}], [{ - component: CollectionFileViewerAction, - execute: () => { return; }, -}]]; +]]); \ No newline at end of file diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index 65e98cc5..55b0abd8 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -70,11 +70,14 @@ export enum ContextMenuKind { FAVORITE = "Favorite", TRASH = "Trash", COLLECTION_FILES = "CollectionFiles", + READONLY_COLLECTION_FILES = "ReadOnlyCollectionFiles", COLLECTION_FILES_ITEM = "CollectionFilesItem", + READONLY_COLLECTION_FILES_ITEM = "ReadOnlyCollectionFilesItem", COLLECTION_FILES_NOT_SELECTED = "CollectionFilesNotSelected", COLLECTION = 'Collection', COLLECTION_ADMIN = 'CollectionAdmin', COLLECTION_RESOURCE = 'CollectionResource', + READONLY_COLLECTION = 'ReadOnlyCollection', TRASHED_COLLECTION = 'TrashedCollection', PROCESS = "Process", PROCESS_ADMIN = 'ProcessAdmin', diff --git a/src/views-components/resource-properties-form/property-key-field.tsx b/src/views-components/resource-properties-form/property-key-field.tsx index 1f921188..d17f50d4 100644 --- a/src/views-components/resource-properties-form/property-key-field.tsx +++ b/src/views-components/resource-properties-form/property-key-field.tsx @@ -16,11 +16,13 @@ export const PROPERTY_KEY_FIELD_ID = 'keyID'; export const PropertyKeyField = connectVocabulary( ({ vocabulary, skipValidation }: VocabularyProp & ValidationProp) => + + ); const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) => diff --git a/src/views-components/resource-properties-form/property-value-field.tsx b/src/views-components/resource-properties-form/property-value-field.tsx index 99745199..c5a5071f 100644 --- a/src/views-components/resource-properties-form/property-value-field.tsx +++ b/src/views-components/resource-properties-form/property-value-field.tsx @@ -28,11 +28,13 @@ const connectVocabularyAndPropertyKey = compose( export const PropertyValueField = connectVocabularyAndPropertyKey( ({ skipValidation, ...props }: PropertyValueFieldProps) => + + ); const PropertyValueInput = ({ vocabulary, propertyKey, ...props }: WrappedFieldProps & PropertyValueFieldProps) => diff --git a/src/views-components/resource-properties-form/resource-properties-form.tsx b/src/views-components/resource-properties-form/resource-properties-form.tsx index db40e4a7..0632b97c 100644 --- a/src/views-components/resource-properties-form/resource-properties-form.tsx +++ b/src/views-components/resource-properties-form/resource-properties-form.tsx @@ -20,7 +20,7 @@ export interface ResourcePropertiesFormData { export type ResourcePropertiesFormProps = InjectedFormProps & WithStyles; export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, classes }: ResourcePropertiesFormProps ) => -
+ diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx index c4221937..36625387 100644 --- a/src/views/collection-panel/collection-panel.tsx +++ b/src/views/collection-panel/collection-panel.tsx @@ -11,7 +11,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 } from '~/components/icon/icon'; +import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon } 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'; @@ -25,8 +25,11 @@ 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'; import { IllegalNamingWarning } from '~/components/warning/warning'; +import { GroupResource } from '~/models/group'; +import { UserResource } from '~/models/user'; +import { getUserUuid } from '~/common/getuser'; -type CssRules = 'card' | 'iconHeader' | 'tag' | 'label' | 'value' | 'link'; +type CssRules = 'card' | 'iconHeader' | 'tag' | 'label' | 'value' | 'link' | 'centeredLabel' | 'readOnlyIcon'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ card: { @@ -43,6 +46,10 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ label: { fontSize: '0.875rem' }, + centeredLabel: { + fontSize: '0.875rem', + textAlign: 'center' + }, value: { textTransform: 'none', fontSize: '0.875rem' @@ -53,11 +60,16 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ '&:hover': { cursor: 'pointer' } + }, + readOnlyIcon: { + marginLeft: theme.spacing.unit, + fontSize: 'small', } }); interface CollectionPanelDataProps { item: CollectionResource; + isWritable: boolean; } type CollectionPanelProps = CollectionPanelDataProps & DispatchProp @@ -65,16 +77,25 @@ type CollectionPanelProps = CollectionPanelDataProps & DispatchProp export const CollectionPanel = withStyles(styles)( connect((state: RootState, props: RouteComponentProps<{ id: string }>) => { - const item = getResource(props.match.params.id)(state.resources); - return { item }; + const currentUserUUID = getUserUuid(state); + const item = getResource(props.match.params.id)(state.resources); + let isWritable = false; + if (item && item.ownerUuid === currentUserUUID) { + isWritable = true; + } else if (item) { + const itemOwner = getResource(item.ownerUuid)(state.resources); + if (itemOwner) { + isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0; + } + } + return { item, isWritable }; })( class extends React.Component { - render() { - const { classes, item, dispatch } = this.props; + const { classes, item, dispatch, isWritable } = this.props; return item ? <> - + @@ -84,31 +105,42 @@ export const CollectionPanel = withStyles(styles)( action={ } - title={item && {item.name}} + title={ + + + {item.name} + {isWritable || + + + + } + + } titleTypographyProps={this.titleProps} - subheader={item && item.description} + subheader={item.description} subheaderTypographyProps={this.titleProps} /> + linkToUuid={item.uuid} /> + linkToUuid={item.portableDataHash} /> + label='Number of files' value={item.fileCount} /> + label='Content size' value={formatFileSize(item.fileSizeTotal)} /> + label='Owner' linkToUuid={item.ownerUuid} /> {(item.properties.container_request || item.properties.containerRequest) && dispatch(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}> @@ -119,32 +151,39 @@ export const CollectionPanel = withStyles(styles)( - + - + {isWritable && - + } - {Object.keys(item.properties).map(k => + { 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, - this.handleDelete(k, v), + isWritable + ? this.handleDelete(k, item.properties[k]) + : undefined, classes.tag)) : getPropertyChip( k, item.properties[k], - this.handleDelete(k, item.properties[k]), + isWritable + ? this.handleDelete(k, item.properties[k]) + : undefined, classes.tag) - )} + ) + :
No properties set on this collection.
+ }
- +
: null; @@ -152,15 +191,18 @@ export const CollectionPanel = withStyles(styles)( handleContextMenu = (event: React.MouseEvent) => { const { uuid, ownerUuid, name, description, kind, isTrashed } = this.props.item; + const { isWritable } = this.props; const resource = { uuid, ownerUuid, name, description, kind, - menuKind: isTrashed - ? ContextMenuKind.TRASHED_COLLECTION - : ContextMenuKind.COLLECTION + menuKind: isWritable + ? isTrashed + ? ContextMenuKind.TRASHED_COLLECTION + : ContextMenuKind.COLLECTION + : ContextMenuKind.READONLY_COLLECTION }; this.props.dispatch(openContextMenu(event, resource)); } diff --git a/yarn.lock b/yarn.lock index da2629f5..d6677a74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -50,6 +50,32 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@fortawesome/fontawesome-common-types@^0.2.28": + version "0.2.28" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz#1091bdfe63b3f139441e9cba27aa022bff97d8b2" + integrity sha512-gtis2/5yLdfI6n0ia0jH7NJs5i/Z/8M/ZbQL6jXQhCthEOe5Cr5NcQPhgTvFxNOtURE03/ZqUcEskdn2M+QaBg== + +"@fortawesome/fontawesome-svg-core@1.2.28": + version "1.2.28" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.28.tgz#e5b8c8814ef375f01f5d7c132d3c3a2f83a3abf9" + integrity sha512-4LeaNHWvrneoU0i8b5RTOJHKx7E+y7jYejplR7uSVB34+mp3Veg7cbKk7NBCLiI4TyoWS1wh9ZdoyLJR8wSAdg== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.28" + +"@fortawesome/free-solid-svg-icons@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.0.tgz#44d9118668ad96b4fd5c9434a43efc5903525739" + integrity sha512-IHUgDJdomv6YtG4p3zl1B5wWf9ffinHIvebqQOmV3U+3SLw4fC+LUCCgwfETkbTtjy5/Qws2VoVf6z/ETQpFpg== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.28" + +"@fortawesome/react-fontawesome@0.1.9": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.9.tgz#c865b9286c707407effcec99958043711367cd02" + integrity sha512-49V3WNysLZU5fZ3sqSuys4nGRytsrxJktbv3vuaXkEoxv22C6T7TEG0TW6+nqVjMnkfCQd5xOnmJoZHMF78tOw== + dependencies: + prop-types "^15.7.2" + "@hapi/address@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.0.1.tgz#267301ddf7bc453718377a6fb3832a2f04a721dd"