Merge branch '17016-delete-single-file-deletes-whole-collection'
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Mon, 11 Jan 2021 15:58:44 +0000 (16:58 +0100)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Mon, 11 Jan 2021 15:58:44 +0000 (16:58 +0100)
closes #17016

Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

40 files changed:
cypress/integration/collection-panel.spec.js
cypress/integration/search.spec.js
package.json
src/components/details-attribute/details-attribute.tsx
src/components/file-tree/file-thumbnail.test.tsx [new file with mode: 0644]
src/components/file-tree/file-thumbnail.tsx
src/components/icon/icon.tsx
src/index.tsx
src/services/collection-service/collection-service.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/store/collections/collection-info-actions.ts [new file with mode: 0644]
src/store/collections/collection-version-actions.ts [new file with mode: 0644]
src/store/context-menu/context-menu-actions.test.ts
src/store/context-menu/context-menu-actions.ts
src/store/resources/resources.ts
src/store/run-process-panel/run-process-panel-actions.test.ts
src/store/run-process-panel/run-process-panel-actions.ts
src/views-components/collections-dialog/restore-version-dialog.ts [new file with mode: 0644]
src/views-components/context-menu/action-sets/collection-action-set.ts
src/views-components/context-menu/action-sets/collection-admin-action-set.ts [deleted file]
src/views-components/context-menu/action-sets/collection-resource-action-set.ts [deleted file]
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/context-menu/action-sets/project-admin-action-set.ts
src/views-components/context-menu/context-menu.tsx
src/views-components/details-panel/collection-details.tsx
src/views-components/webdav-s3-dialog/webdav-s3-dialog.test.tsx [new file with mode: 0644]
src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx [new file with mode: 0644]
src/views/collection-content-address-panel/collection-content-address-panel.tsx
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/link-panel/link-panel.tsx
src/views/main-panel/main-panel-root.tsx
src/views/project-panel/project-panel.tsx
src/views/public-favorites-panel/public-favorites-panel.tsx
src/views/run-process-panel/run-process-advanced-form.tsx
src/views/shared-with-me-panel/shared-with-me-panel.tsx
src/views/virtual-machine-panel/virtual-machine-user-panel.tsx
src/views/workbench/workbench.tsx
tools/arvados_config.yml
yarn.lock

index 377f11d9c9882c258fb67e9448e55c70705576c0..b86ff2a7d39143fe6c3d66c813abdfaec386085c 100644 (file)
@@ -52,6 +52,7 @@ describe('Collection panel tests', function() {
                     })
                     cy.visit(`/collections/${this.testCollection.uuid}`);
                     // Check that name & uuid are correct.
+                    cy.get('[data-cy=linear-progress]').should('not.exist');
                     cy.get('[data-cy=collection-info-panel]')
                         .should('contain', this.testCollection.name)
                         .and('contain', this.testCollection.uuid)
@@ -146,6 +147,7 @@ describe('Collection panel tests', function() {
                 ['I ❤️ ⛵️', '...']
             ];
             nameTransitions.forEach(([from, to]) => {
+                cy.get('[data-cy=linear-progress]').should('not.exist');
                 cy.get('[data-cy=collection-files-panel]')
                     .contains(`${from}`).rightclick();
                 cy.get('[data-cy=context-menu]')
@@ -175,6 +177,7 @@ describe('Collection panel tests', function() {
             cy.loginAs(activeUser);
             cy.visit(`/collections/${this.testCollection.uuid}`);
             // Rename 'bar' to 'subdir/foo'
+            cy.get('[data-cy=linear-progress]').should('not.exist');
             cy.get('[data-cy=collection-files-panel]')
                 .contains('bar').rightclick();
             cy.get('[data-cy=context-menu]')
@@ -231,6 +234,7 @@ describe('Collection panel tests', function() {
                 ['//foo', 'Empty dir name not allowed']
             ]
             illegalNamesFromUI.forEach(([name, errMsg]) => {
+                cy.get('[data-cy=linear-progress]').should('not.exist');
                 cy.get('[data-cy=collection-files-panel]')
                     .contains('bar').rightclick();
                 cy.get('[data-cy=context-menu]')
@@ -269,6 +273,7 @@ describe('Collection panel tests', function() {
         cy.createCollection(adminUser.token, {
             name: colName,
             owner_uuid: activeUser.user.uuid,
+            preserve_version: true,
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
         .as('originalVersion').then(function() {
             // Change the file name to create a new version.
@@ -309,6 +314,7 @@ describe('Collection panel tests', function() {
         cy.createCollection(adminUser.token, {
             name: colName,
             owner_uuid: activeUser.user.uuid,
+            preserve_version: true,
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"})
         .as('collection').then(function() {
             // Visit collection, check basic information
@@ -352,6 +358,13 @@ describe('Collection panel tests', function() {
             cy.get('[data-cy=collection-files-panel]')
                 .should('contain', 'foo').and('contain', 'bar');
 
+            // Check that only old collection action are available on context menu
+            cy.get('[data-cy=collection-panel-options-btn]').click();
+            cy.get('[data-cy=context-menu]')
+                .should('contain', 'Restore version')
+                .and('not.contain', 'Add to favorites');
+            cy.get('body').click(); // Collapse the menu avoiding details panel expansion
+
             // Click on "head version" link, confirm that it's the latest version.
             cy.get('[data-cy=collection-info-panel]').contains('head version').click();
             cy.get('[data-cy=collection-info-panel]')
@@ -362,6 +375,11 @@ describe('Collection panel tests', function() {
             cy.get('[data-cy=collection-files-panel]').
                 should('not.contain', 'foo').and('contain', 'bar');
 
+            // Check that old collection action isn't available on context menu
+            cy.get('[data-cy=collection-panel-options-btn]').click()
+            cy.get('[data-cy=context-menu]').should('not.contain', 'Restore version')
+            cy.get('body').click(); // Collapse the menu avoiding details panel expansion
+
             // Make another change, confirm new version.
             cy.get('[data-cy=collection-panel-options-btn]').click();
             cy.get('[data-cy=context-menu]').contains('Edit collection').click();
@@ -392,10 +410,25 @@ describe('Collection panel tests', function() {
             // (and now an old version...)
             cy.get('[data-cy=collection-version-browser-select-1]').rightclick()
             cy.get('[data-cy=context-menu]')
-                .should('contain', 'Add to favorites')
+                .should('not.contain', 'Add to favorites')
                 .and('contain', 'Make a copy')
                 .and('not.contain', 'Edit collection');
             cy.get('body').click();
+
+            // Restore first version
+            cy.get('[data-cy=collection-version-browser]').within(() => {
+                cy.get('[data-cy=collection-version-browser-select-1]').click();
+            });
+            cy.get('[data-cy=collection-panel-options-btn]').click()
+            cy.get('[data-cy=context-menu]').contains('Restore version').click();
+            cy.get('[data-cy=confirmation-dialog]').should('contain', 'Restore version');
+            cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+            cy.get('[data-cy=collection-info-panel]')
+                .should('not.contain', 'This is an old version');
+            cy.get('[data-cy=collection-version-number]').should('contain', '4');
+            cy.get('[data-cy=collection-info-panel]').should('contain', colName);
+            cy.get('[data-cy=collection-files-panel]')
+                .should('contain', 'foo').and('contain', 'bar');
         });
     });
 })
index 60292aa50e3edfb927605f1346a126b984420700..06131a2ef09c3966f8e009c661e78aea0fb174f9 100644 (file)
@@ -46,6 +46,7 @@ describe('Search tests', function() {
         cy.createCollection(adminUser.token, {
             name: colName,
             owner_uuid: activeUser.user.uuid,
+            preserve_version: true,
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
         .as('originalVersion').then(function() {
             // Change the file name to create a new version.
index 346d4910c19bb51899bf6e71f5626ae5e05aeb5b..c972ff02377218afa2de4afb58cf0a7cafb32676 100644 (file)
@@ -97,6 +97,7 @@
     "@types/react-router-dom": "4.3.1",
     "@types/react-router-redux": "5.0.16",
     "@types/redux-devtools": "3.0.44",
+    "@types/redux-mock-store": "1.0.2",
     "@types/sinon": "7.5",
     "@types/uuid": "3.4.4",
     "axios-mock-adapter": "1.17.0",
     "node-sass": "4.9.4",
     "node-sass-chokidar": "1.3.4",
     "redux-devtools": "3.4.1",
+    "redux-mock-store": "1.5.4",
     "typescript": "3.1.1",
     "wait-on": "4.0.2",
     "yamljs": "0.3.0"
index 4b8ee8378fa32c8fc3db839629780b2ebc133545..01276c57edbfc3d07aa2a00a75e432a3844732b0 100644 (file)
@@ -61,6 +61,7 @@ interface DetailsAttributeDataProps {
     children?: React.ReactNode;
     onValueClick?: () => void;
     linkToUuid?: string;
+    copyValue?: string;
 }
 
 type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules> & FederationConfig & DispatchProp;
@@ -85,7 +86,7 @@ export const DetailsAttribute = connect(mapStateToProps)(withStyles(styles)(
         render() {
             const { label, link, value, children, classes, classLabel,
                 classValue, lowercaseValue, onValueClick, linkToUuid,
-                localCluster, remoteHostsConfig, sessions } = this.props;
+                localCluster, remoteHostsConfig, sessions, copyValue } = this.props;
             let valueNode: React.ReactNode;
 
             if (linkToUuid) {
@@ -108,9 +109,9 @@ export const DetailsAttribute = connect(mapStateToProps)(withStyles(styles)(
                     className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
                     {valueNode}
                     {children}
-                    {linkToUuid && <Tooltip title="Copy">
+                    {(linkToUuid || copyValue) && <Tooltip title="Copy to clipboard">
                         <span className={classes.copyIcon}>
-                            <CopyToClipboard text={linkToUuid || ""} onCopy={() => this.onCopy("Copied")}>
+                            <CopyToClipboard text={linkToUuid || copyValue || ""} onCopy={() => this.onCopy("Copied")}>
                                 <CopyIcon />
                             </CopyToClipboard>
                         </span>
diff --git a/src/components/file-tree/file-thumbnail.test.tsx b/src/components/file-tree/file-thumbnail.test.tsx
new file mode 100644 (file)
index 0000000..e0d5d25
--- /dev/null
@@ -0,0 +1,33 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { shallow, configure } from "enzyme";
+import { FileThumbnail } from "./file-thumbnail";
+import { CollectionFileType } from '../../models/collection-file';
+import * as Adapter from 'enzyme-adapter-react-16';
+
+configure({ adapter: new Adapter() });
+
+jest.mock('is-image', () => ({
+    'default': () => true,
+}));
+
+describe("<FileThumbnail />", () => {
+    let file;
+
+    beforeEach(() => {
+        file = {
+            name: 'test-image',
+            type: CollectionFileType.FILE,
+            url: 'http://example.com/c=zzzzz-4zz18-0123456789abcde/t=v2/zzzzz-gj3su-0123456789abcde/xxxxxxtokenxxxxx/test-image.jpg',
+            size: 300
+        };
+    });
+
+    it("renders file thumbnail with proper src", () => {
+        const fileThumbnail = shallow(<FileThumbnail file={file} />);
+        expect(fileThumbnail.html()).toBe('<img class="Component-thumbnail-1" alt="test-image" src="http://example.com/c=zzzzz-4zz18-0123456789abcde/test-image.jpg?api_token=v2/zzzzz-gj3su-0123456789abcde/xxxxxxtokenxxxxx"/>');
+    });
+});
index e1a0d5e0517dea73a0a2cbea9910f849841fd5cd..40631961a8a526143c7d7c6b3835945adb940284 100644 (file)
@@ -7,6 +7,7 @@ import isImage from 'is-image';
 import { withStyles, WithStyles } from '@material-ui/core';
 import { FileTreeData } from '~/components/file-tree/file-tree-data';
 import { CollectionFileType } from '~/models/collection-file';
+import { sanitizeToken } from "~/views-components/context-menu/actions/helpers";
 
 export interface FileThumbnailProps {
     file: FileTreeData;
@@ -32,5 +33,5 @@ const ImageFileThumbnail = imageFileThumbnailStyle(
         <img
             className={classes.thumbnail}
             alt={file.name}
-            src={file.url} />
+            src={sanitizeToken(file.url)} />
 );
index 55c3c5a50f44759f10bbee681299b5ebe92cc32a..bceee5043589c0d8b94b0e278fe8543177ff373e 100644 (file)
@@ -24,7 +24,9 @@ import DeviceHub from '@material-ui/icons/DeviceHub';
 import Edit from '@material-ui/icons/Edit';
 import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded';
 import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import FlipToFront from '@material-ui/icons/FlipToFront';
 import Folder from '@material-ui/icons/Folder';
+import FolderShared from '@material-ui/icons/FolderShared';
 import GetApp from '@material-ui/icons/GetApp';
 import Help from '@material-ui/icons/Help';
 import HelpOutline from '@material-ui/icons/HelpOutline';
@@ -66,7 +68,7 @@ library.add(
     faSlash,
 );
 
-export const ReadOnlyIcon = (props:any) =>
+export const ReadOnlyIcon = (props: any) =>
     <span {...props}>
         <div className="fa-layers fa-1x fa-fw">
             <span className="fas fa-slash"
@@ -102,7 +104,7 @@ 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 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} />;
@@ -129,6 +131,7 @@ export const RemoveIcon: IconType = (props) => <Delete {...props} />;
 export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
 export const PublicFavoriteIcon: IconType = (props) => <Public {...props} />;
 export const RenameIcon: IconType = (props) => <Edit {...props} />;
+export const RestoreVersionIcon: IconType = (props) => <FlipToFront {...props} />;
 export const RestoreFromTrashIcon: IconType = (props) => <RestoreFromTrash {...props} />;
 export const ReRunProcessIcon: IconType = (props) => <Cached {...props} />;
 export const SearchIcon: IconType = (props) => <Search {...props} />;
@@ -141,3 +144,4 @@ export const UsedByIcon: IconType = (props) => <Folder {...props} />;
 export const WorkflowIcon: IconType = (props) => <Code {...props} />;
 export const WarningIcon: IconType = (props) => <Warning style={{ color: '#fbc02d', height: '30px', width: '30px' }} {...props} />;
 export const Link: IconType = (props) => <LinkOutlined {...props} />;
+export const FolderSharedIcon: IconType = (props) => <FolderShared {...props} />;
index 569656d9117874646b238616330e25d640aa932e..98281b67d9ff5cfb4bca863ea7b5b5f2dd8ce27d 100644 (file)
@@ -27,7 +27,7 @@ import { favoriteActionSet } from "~/views-components/context-menu/action-sets/f
 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, readOnlyCollectionActionSet } from '~/views-components/context-menu/action-sets/collection-action-set';
+import { collectionActionSet, collectionAdminActionSet, oldCollectionVersionActionSet, readOnlyCollectionActionSet } from '~/views-components/context-menu/action-sets/collection-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';
@@ -57,7 +57,6 @@ import { groupActionSet } from '~/views-components/context-menu/action-sets/grou
 import { groupMemberActionSet } from '~/views-components/context-menu/action-sets/group-member-action-set';
 import { linkActionSet } from '~/views-components/context-menu/action-sets/link-action-set';
 import { loadFileViewersConfig } from '~/store/file-viewers/file-viewers-actions';
-import { collectionAdminActionSet } from '~/views-components/context-menu/action-sets/collection-admin-action-set';
 import { processResourceAdminActionSet } from '~/views-components/context-menu/action-sets/process-resource-admin-action-set';
 import { projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
@@ -78,6 +77,7 @@ addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActio
 addMenuActionSet(ContextMenuKind.READONLY_COLLECTION_FILES_ITEM, readOnlyCollectionFilesItemActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
 addMenuActionSet(ContextMenuKind.READONLY_COLLECTION, readOnlyCollectionActionSet);
+addMenuActionSet(ContextMenuKind.OLD_VERSION_COLLECTION, oldCollectionVersionActionSet);
 addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
index 5ae03b89eb0f816c7f595d322dadd1000d94b8cd..c46c3e27764470f1c214cc4ff82200d612b6a8c4 100644 (file)
@@ -27,6 +27,14 @@ export class CollectionService extends TrashableResourceService<CollectionResour
         ]);
     }
 
+    create(data?: Partial<CollectionResource>) {
+        return super.create({ ...data, preserveVersion: true });
+    }
+
+    update(uuid: string, data: Partial<CollectionResource>) {
+        return super.update(uuid, { ...data, preserveVersion: true });
+    }
+
     async files(uuid: string) {
         const request = await this.webdavClient.propfind(`c=${uuid}`);
         if (request.responseXML != null) {
@@ -55,20 +63,24 @@ export class CollectionService extends TrashableResourceService<CollectionResour
                 await this.webdavClient.delete(`c=${path}`);
             }
         }
+        await this.update(collectionUuid, { preserveVersion: true });
     }
 
     async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress) {
+        if (collectionUuid === "" || files.length === 0) { return; }
         // files have to be uploaded sequentially
         for (let idx = 0; idx < files.length; idx++) {
             await this.uploadFile(collectionUuid, files[idx], idx, onProgress);
         }
+        await this.update(collectionUuid, { preserveVersion: true });
     }
 
-    moveFile(collectionUuid: string, oldPath: string, newPath: string) {
-        return this.webdavClient.move(
+    async moveFile(collectionUuid: string, oldPath: string, newPath: string) {
+        await this.webdavClient.move(
             `c=${collectionUuid}${oldPath}`,
             `c=${collectionUuid}${encodeURI(newPath)}`
         );
+        await this.update(collectionUuid, { preserveVersion: true });
     }
 
     extendFileURL = (file: CollectionDirectory | CollectionFile) => {
index 373ee04eec06e3735d5c469668352394c274de85..c7a3bdc50372fdd457de1df63b7523174592f1ea 100644 (file)
@@ -42,10 +42,10 @@ export const loadCollectionFiles = (uuid: string) =>
             const mapped = mapTreeValues(services.collectionService.extendFileURL)(sorted);
             dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(mapped));
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PANEL_LOAD_FILES));
-        }).catch(e => {
+        }).catch(() => {
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PANEL_LOAD_FILES));
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: `Error getting file list: ${e.errors[0]}`,
+                message: `Error getting file list`,
                 hideDuration: 2000,
                 kind: SnackbarKind.ERROR
             }));
diff --git a/src/store/collections/collection-info-actions.ts b/src/store/collections/collection-info-actions.ts
new file mode 100644 (file)
index 0000000..49fe54f
--- /dev/null
@@ -0,0 +1,39 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from "~/store/store";
+import { ServiceRepository } from "~/services/services";
+import { dialogActions } from '~/store/dialog/dialog-actions';
+
+export const COLLECTION_WEBDAV_S3_DIALOG_NAME = 'collectionWebdavS3Dialog';
+
+export interface WebDavS3InfoDialogData {
+    uuid: string;
+    token: string;
+    downloadUrl: string;
+    collectionsUrl: string;
+    localCluster: string;
+    username: string;
+    activeTab: number;
+    setActiveTab: (event: any, tabNr: number) => void;
+}
+
+export const openWebDavS3InfoDialog = (uuid: string, activeTab?: number) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(dialogActions.OPEN_DIALOG({
+            id: COLLECTION_WEBDAV_S3_DIALOG_NAME,
+            data: {
+                title: 'Access Collection using WebDAV or S3',
+                token: getState().auth.apiToken,
+                downloadUrl: getState().auth.config.keepWebServiceUrl,
+                collectionsUrl: getState().auth.config.keepWebInlineServiceUrl,
+                localCluster: getState().auth.localCluster,
+                username: getState().auth.user!.username,
+                activeTab: activeTab || 0,
+                setActiveTab: (event: any, tabNr: number) => dispatch<any>(openWebDavS3InfoDialog(uuid, tabNr)),
+                uuid
+            }
+        }));
+    };
diff --git a/src/store/collections/collection-version-actions.ts b/src/store/collections/collection-version-actions.ts
new file mode 100644 (file)
index 0000000..5bf4749
--- /dev/null
@@ -0,0 +1,48 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
+import { resourcesActions } from "../resources/resources-actions";
+import { navigateTo } from "../navigation/navigation-action";
+import { dialogActions } from "../dialog/dialog-actions";
+
+export const COLLECTION_RESTORE_VERSION_DIALOG = 'collectionRestoreVersionDialog';
+
+export const openRestoreCollectionVersionDialog = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(dialogActions.OPEN_DIALOG({
+            id: COLLECTION_RESTORE_VERSION_DIALOG,
+            data: {
+                title: 'Restore version',
+                text: "This will copy the content of the selected version to the head. To make a new collection with the content of the selected version, use 'Make a copy' instead.",
+                confirmButtonLabel: 'Restore',
+                uuid
+            }
+        }));
+    };
+
+export const restoreVersion = (resourceUuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        try {
+            // Request que entire record because stored old versions usually
+            // don't include the manifest_text field.
+            const oldVersion = await services.collectionService.get(resourceUuid);
+            const { uuid, version, ...rest} = oldVersion;
+            const headVersion = await services.collectionService.update(
+                oldVersion.currentVersionUuid,
+                { ...rest }
+            );
+            dispatch(resourcesActions.SET_RESOURCES([headVersion]));
+            dispatch<any>(navigateTo(headVersion.uuid));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: `Couldn't restore version: ${e.errors[0]}`,
+                hideDuration: 2000,
+                kind: SnackbarKind.ERROR
+            }));
+        }
+    };
index c3e78679278f69783ff5c7a055da5def03217729..2778568e7681d1073a7f627e70bf9444b8aaee21 100644 (file)
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as resource from '~/models/resource';
 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-import { resourceKindToContextMenuKind } from './context-menu-actions';
+import { resourceUuidToContextMenuKind } from './context-menu-actions';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
 
 describe('context-menu-actions', () => {
-    describe('resourceKindToContextMenuKind', () => {
-        const uuid = '123';
-
-        describe('ResourceKind.PROJECT', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.PROJECT);
-            });
-
-            it('should return ContextMenuKind.PROJECT_ADMIN', () => {
-                // given
-                const isAdmin = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.PROJECT_ADMIN);
-            });
-
-            it('should return ContextMenuKind.PROJECT', () => {
-                // given
-                const isAdmin = false;
-                const isEditable = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.PROJECT);
-            });
-
-            it('should return ContextMenuKind.READONLY_PROJECT', () => {
-                // given
-                const isAdmin = false;
-                const isEditable = false;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.READONLY_PROJECT);
-            });
-        });
-
-        describe('ResourceKind.COLLECTION', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.COLLECTION);
-            });
-
-            it('should return ContextMenuKind.COLLECTION_ADMIN', () => {
-                // given
-                const isAdmin = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.COLLECTION_ADMIN);
-            });
-
-            it('should return ContextMenuKind.COLLECTION', () => {
-                // given
-                const isAdmin = false;
-                const isEditable = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.COLLECTION);
-            });
-
-            it('should return ContextMenuKind.READONLY_COLLECTION', () => {
-                // given
-                const isAdmin = false;
-                const isEditable = false;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.READONLY_COLLECTION);
-            });
-        });
-
-        describe('ResourceKind.PROCESS', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.PROCESS);
-            });
-
-            it('should return ContextMenuKind.PROCESS_ADMIN', () => {
-                // given
-                const isAdmin = true;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.PROCESS_ADMIN);
-            });
-
-            it('should return ContextMenuKind.PROCESS_RESOURCE', () => {
-                // given
-                const isAdmin = false;
-
-                // when
-                const result = resourceKindToContextMenuKind(uuid, isAdmin);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.PROCESS_RESOURCE);
-            });
-        });
-
-        describe('ResourceKind.USER', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.USER);
-            });
-
-            it('should return ContextMenuKind.ROOT_PROJECT', () => {
-                // when
-                const result = resourceKindToContextMenuKind(uuid);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.ROOT_PROJECT);
-            });
-        });
-
-        describe('ResourceKind.LINK', () => {
-            beforeEach(() => {
-                // setup
-                jest.spyOn(resource, 'extractUuidKind')
-                    .mockImplementation(() => resource.ResourceKind.LINK);
-            });
-
-            it('should return ContextMenuKind.LINK', () => {
-                // when
-                const result = resourceKindToContextMenuKind(uuid);
-
-                // then
-                expect(result).toEqual(ContextMenuKind.LINK);
+    describe('resourceUuidToContextMenuKind', () => {
+        const middlewares = [thunk];
+        const mockStore = configureStore(middlewares);
+        const userUuid = 'zzzzz-tpzed-bbbbbbbbbbbbbbb';
+        const otherUserUuid = 'zzzzz-tpzed-bbbbbbbbbbbbbbc';
+        const headCollectionUuid = 'zzzzz-4zz18-aaaaaaaaaaaaaaa';
+        const oldCollectionUuid = 'zzzzz-4zz18-aaaaaaaaaaaaaab';
+        const projectUuid = 'zzzzz-j7d0g-ccccccccccccccc';
+        const linkUuid = 'zzzzz-o0j2j-0123456789abcde';
+        const containerRequestUuid = 'zzzzz-xvhdp-0123456789abcde';
+
+        it('should return the correct menu kind', () => {
+            const cases = [
+                // resourceUuid, isAdminUser, isEditable, isTrashed, expected
+                [headCollectionUuid, false, true, true, ContextMenuKind.TRASHED_COLLECTION],
+                [headCollectionUuid, false, true, false, ContextMenuKind.COLLECTION],
+                [headCollectionUuid, false, false, true, ContextMenuKind.READONLY_COLLECTION],
+                [headCollectionUuid, false, false, false, ContextMenuKind.READONLY_COLLECTION],
+                [headCollectionUuid, true, true, true, ContextMenuKind.TRASHED_COLLECTION],
+                [headCollectionUuid, true, true, false, ContextMenuKind.COLLECTION_ADMIN],
+                [headCollectionUuid, true, false, true, ContextMenuKind.TRASHED_COLLECTION],
+                [headCollectionUuid, true, false, false, ContextMenuKind.COLLECTION_ADMIN],
+
+                [oldCollectionUuid, false, true, true, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, false, true, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, true, true, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, false, true, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+
+                // FIXME: WB2 doesn't currently have context menu for trashed projects
+                // [projectUuid, false, true, true, ContextMenuKind.TRASHED_PROJECT],
+                [projectUuid, false, true, false, ContextMenuKind.PROJECT],
+                [projectUuid, false, false, true, ContextMenuKind.READONLY_PROJECT],
+                [projectUuid, false, false, false, ContextMenuKind.READONLY_PROJECT],
+                // [projectUuid, true, true, true, ContextMenuKind.TRASHED_PROJECT],
+                [projectUuid, true, true, false, ContextMenuKind.PROJECT_ADMIN],
+                // [projectUuid, true, false, true, ContextMenuKind.TRASHED_PROJECT],
+                [projectUuid, true, false, false, ContextMenuKind.PROJECT_ADMIN],
+
+                [linkUuid, false, true, true, ContextMenuKind.LINK],
+                [linkUuid, false, true, false, ContextMenuKind.LINK],
+                [linkUuid, false, false, true, ContextMenuKind.LINK],
+                [linkUuid, false, false, false, ContextMenuKind.LINK],
+                [linkUuid, true, true, true, ContextMenuKind.LINK],
+                [linkUuid, true, true, false, ContextMenuKind.LINK],
+                [linkUuid, true, false, true, ContextMenuKind.LINK],
+                [linkUuid, true, false, false, ContextMenuKind.LINK],
+
+                [userUuid, false, true, true, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, true, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, false, true, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, false, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, true, true, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, true, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, false, true, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, false, false, ContextMenuKind.ROOT_PROJECT],
+
+                [containerRequestUuid, false, true, true, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, true, false, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, false, true, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, false, false, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, true, true, true, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, true, false, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, false, true, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, false, false, ContextMenuKind.PROCESS_ADMIN],
+            ]
+
+            cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, expected]) => {
+                const initialState = {
+                    resources: {
+                        [headCollectionUuid]: {
+                            uuid: headCollectionUuid,
+                            ownerUuid: projectUuid,
+                            currentVersionUuid: headCollectionUuid,
+                            isTrashed: isTrashed,
+                        },
+                        [oldCollectionUuid]: {
+                            uuid: oldCollectionUuid,
+                            currentVersionUuid: headCollectionUuid,
+                            isTrashed: isTrashed,
+
+                        },
+                        [projectUuid]: {
+                            uuid: projectUuid,
+                            ownerUuid: isEditable ? userUuid : otherUserUuid,
+                            writableBy: isEditable ? [userUuid] : [otherUserUuid],
+                        },
+                        [linkUuid]: {
+                            uuid: linkUuid,
+                        },
+                        [userUuid]: {
+                            uuid: userUuid,
+                        },
+                        [containerRequestUuid]: {
+                            uuid: containerRequestUuid,
+                            ownerUuid: projectUuid,
+                        },
+                    },
+                    auth: {
+                        user: {
+                            uuid: userUuid,
+                            isAdmin: isAdminUser,
+                        },
+                    },
+                };
+                const store = mockStore(initialState);
+
+                const menuKind = store.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid as string))
+                try {
+                    expect(menuKind).toBe(expected);
+                } catch (err) {
+                    throw new Error(`menuKind for resource ${JSON.stringify(initialState.resources[resourceUuid as string])} expected to be ${expected} but got ${menuKind}.`);
+                }
             });
         });
     });
index 308d5e88134520296c0088ffef9c662755647a71..225538859a743a690e2da15143fe600d8e786fe6 100644 (file)
@@ -8,7 +8,6 @@ import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
 import { getResource, getResourceWithEditableStatus } from '../resources/resources';
-import { ProjectResource } from '~/models/project';
 import { UserResource } from '~/models/user';
 import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
 import { extractUuidKind, ResourceKind, EditableResource } from '~/models/resource';
@@ -18,6 +17,9 @@ import { SshKeyResource } from '~/models/ssh-key';
 import { VirtualMachinesResource } from '~/models/virtual-machines';
 import { KeepServiceResource } from '~/models/keep-services';
 import { ProcessResource } from '~/models/process';
+import { CollectionResource } from '~/models/collection';
+import { GroupResource } from '~/models/group';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
@@ -156,9 +158,8 @@ export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>,
 
 export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const { isAdmin, uuid: userUuid } = getState().auth.user!;
-        const res = getResourceWithEditableStatus<ProjectResource & EditableResource>(resourceUuid, userUuid)(getState().resources);
-        const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (res || {} as EditableResource).isEditable);
+        const res = getResource<GroupContentsResource>(resourceUuid)(getState().resources);
+        const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
         if (res && menuKind) {
             dispatch<any>(openContextMenu(event, {
                 name: res.name,
@@ -166,7 +167,7 @@ export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, res
                 kind: res.kind,
                 menuKind,
                 ownerUuid: res.ownerUuid,
-                isTrashed: res.isTrashed
+                isTrashed: ('isTrashed' in res) ? res.isTrashed: false,
             }));
         }
     };
@@ -200,30 +201,42 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, pro
         }
     };
 
-export const resourceKindToContextMenuKind = (uuid: string, isAdmin?: boolean, isEditable?: boolean) => {
-    const kind = extractUuidKind(uuid);
-    switch (kind) {
-        case ResourceKind.PROJECT:
-            return !isAdmin
-                ? isEditable
-                    ? ContextMenuKind.PROJECT
-                    : ContextMenuKind.READONLY_PROJECT
-                : ContextMenuKind.PROJECT_ADMIN;
-        case ResourceKind.COLLECTION:
-            return !isAdmin
-                ? isEditable
-                    ? ContextMenuKind.COLLECTION
-                    : ContextMenuKind.READONLY_COLLECTION
-                : ContextMenuKind.COLLECTION_ADMIN;
-        case ResourceKind.PROCESS:
-            return !isAdmin
-                ? ContextMenuKind.PROCESS_RESOURCE
-                : ContextMenuKind.PROCESS_ADMIN;
-        case ResourceKind.USER:
-            return ContextMenuKind.ROOT_PROJECT;
-        case ResourceKind.LINK:
-            return ContextMenuKind.LINK;
-        default:
-            return;
-    }
-};
+export const resourceUuidToContextMenuKind = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
+        const kind = extractUuidKind(uuid);
+        const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
+        const isEditable = isAdminUser || (resource || {} as EditableResource).isEditable;
+        switch (kind) {
+            case ResourceKind.PROJECT:
+                return !isAdminUser
+                    ? isEditable
+                        ? ContextMenuKind.PROJECT
+                        : ContextMenuKind.READONLY_PROJECT
+                    : ContextMenuKind.PROJECT_ADMIN;
+            case ResourceKind.COLLECTION:
+                const c = getResource<CollectionResource>(uuid)(getState().resources);
+                if (c === undefined) { return; }
+                const isOldVersion = c.uuid !== c.currentVersionUuid;
+                const isTrashed = c.isTrashed;
+                return isOldVersion
+                    ? ContextMenuKind.OLD_VERSION_COLLECTION
+                    : (isTrashed && isEditable)
+                        ? ContextMenuKind.TRASHED_COLLECTION
+                        : isAdminUser
+                            ? ContextMenuKind.COLLECTION_ADMIN
+                            : isEditable
+                                ? ContextMenuKind.COLLECTION
+                                : ContextMenuKind.READONLY_COLLECTION;
+            case ResourceKind.PROCESS:
+                return !isAdminUser
+                    ? ContextMenuKind.PROCESS_RESOURCE
+                    : ContextMenuKind.PROCESS_ADMIN;
+            case ResourceKind.USER:
+                return ContextMenuKind.ROOT_PROJECT;
+            case ResourceKind.LINK:
+                return ContextMenuKind.LINK;
+            default:
+                return;
+        }
+    };
index eb3c5509f2f1c3f99c2afc028c4ae18e7e308b6c..696a136280c1a72fef39a8a204e5fd9557439508 100644 (file)
@@ -31,6 +31,8 @@ const getResourceWritableBy = (state: ResourcesState, id: string, userUuid: stri
 
 export const getResourceWithEditableStatus = <T extends EditableResource & GroupResource>(id: string, userUuid?: string) =>
     (state: ResourcesState): T | undefined => {
+        if (state[id] === undefined) { return; }
+
         const resource = JSON.parse(JSON.stringify(state[id] as T));
 
         if (resource) {
index cfdb75ea7aa997a24b12ba1da251aaf8345eef71..e9191293408ff0e291860cdd9aab0de8b50582d7 100644 (file)
@@ -93,7 +93,7 @@ describe("run-process-panel-actions", () => {
                         description:
                             "Reverse the lines in a document, then sort those lines.",
                         definition:
-                            '{\n    "$graph": [\n        {\n            "class": "Workflow",\n            "doc": "Reverse the lines in a document, then sort those lines.",\n            "id": "#main",\n            "hints":[{"class":"http://arvados.org/cwl#WorkflowRunnerResources","acrContainerImage":"arvados/jobs:2.0.4"}], "inputs": [\n                {\n                    "default": null,\n                    "doc": "The input file to be processed.",\n                    "id": "#main/input",\n                    "type": "File"\n                },\n                {\n                    "default": true,\n                    "doc": "If true, reverse (decending) sort",\n                    "id": "#main/reverse_sort",\n                    "type": "boolean"\n                }\n            ],\n            "outputs": [\n                {\n                    "doc": "The output with the lines reversed and sorted.",\n                    "id": "#main/output",\n                    "outputSource": "#main/sorted/output",\n                    "type": "File"\n                }\n            ],\n            "steps": [\n                {\n                    "id": "#main/rev",\n                    "in": [\n                        {\n                            "id": "#main/rev/input",\n                            "source": "#main/input"\n                        }\n                    ],\n                    "out": [\n                        "#main/rev/output"\n                    ],\n                    "run": "#revtool.cwl"\n                },\n                {\n                    "id": "#main/sorted",\n                    "in": [\n                        {\n                            "id": "#main/sorted/input",\n                            "source": "#main/rev/output"\n                        },\n                        {\n                            "id": "#main/sorted/reverse",\n                            "source": "#main/reverse_sort"\n                        }\n                    ],\n                    "out": [\n                        "#main/sorted/output"\n                    ],\n                    "run": "#sorttool.cwl"\n                }\n            ]\n        },\n        {\n            "baseCommand": "rev",\n            "class": "CommandLineTool",\n            "doc": "Reverse each line using the `rev` command",\n            "hints": [\n                {\n                    "class": "ResourceRequirement",\n                    "ramMin": 8\n                }\n            ],\n            "id": "#revtool.cwl",\n            "inputs": [\n                {\n                    "id": "#revtool.cwl/input",\n                    "inputBinding": {},\n                    "type": "File"\n                }\n            ],\n            "outputs": [\n                {\n                    "id": "#revtool.cwl/output",\n                    "outputBinding": {\n                        "glob": "output.txt"\n                    },\n                    "type": "File"\n                }\n            ],\n            "stdout": "output.txt"\n        },\n        {\n            "baseCommand": "sort",\n            "class": "CommandLineTool",\n            "doc": "Sort lines using the `sort` command",\n            "hints": [\n                {\n                    "class": "ResourceRequirement",\n                    "ramMin": 8\n                }\n            ],\n            "id": "#sorttool.cwl",\n            "inputs": [\n                {\n                    "id": "#sorttool.cwl/reverse",\n                    "inputBinding": {\n                        "position": 1,\n                        "prefix": "-r"\n                    },\n                    "type": "boolean"\n                },\n                {\n                    "id": "#sorttool.cwl/input",\n                    "inputBinding": {\n                        "position": 2\n                    },\n                    "type": "File"\n                }\n            ],\n            "outputs": [\n                {\n                    "id": "#sorttool.cwl/output",\n                    "outputBinding": {\n                        "glob": "output.txt"\n                    },\n                    "type": "File"\n                }\n            ],\n            "stdout": "output.txt"\n        }\n    ],\n    "cwlVersion": "v1.0"\n}',
+                            '{\n    "$graph": [\n        {\n            "class": "Workflow",\n            "doc": "Reverse the lines in a document, then sort those lines.",\n            "id": "#main",\n            "hints":[{"class":"http://arvados.org/cwl#WorkflowRunnerResources","acrContainerImage":"arvados/jobs:2.0.4", "ramMin": 16000}], "inputs": [\n                {\n                    "default": null,\n                    "doc": "The input file to be processed.",\n                    "id": "#main/input",\n                    "type": "File"\n                },\n                {\n                    "default": true,\n                    "doc": "If true, reverse (decending) sort",\n                    "id": "#main/reverse_sort",\n                    "type": "boolean"\n                }\n            ],\n            "outputs": [\n                {\n                    "doc": "The output with the lines reversed and sorted.",\n                    "id": "#main/output",\n                    "outputSource": "#main/sorted/output",\n                    "type": "File"\n                }\n            ],\n            "steps": [\n                {\n                    "id": "#main/rev",\n                    "in": [\n                        {\n                            "id": "#main/rev/input",\n                            "source": "#main/input"\n                        }\n                    ],\n                    "out": [\n                        "#main/rev/output"\n                    ],\n                    "run": "#revtool.cwl"\n                },\n                {\n                    "id": "#main/sorted",\n                    "in": [\n                        {\n                            "id": "#main/sorted/input",\n                            "source": "#main/rev/output"\n                        },\n                        {\n                            "id": "#main/sorted/reverse",\n                            "source": "#main/reverse_sort"\n                        }\n                    ],\n                    "out": [\n                        "#main/sorted/output"\n                    ],\n                    "run": "#sorttool.cwl"\n                }\n            ]\n        },\n        {\n            "baseCommand": "rev",\n            "class": "CommandLineTool",\n            "doc": "Reverse each line using the `rev` command",\n            "hints": [\n                {\n                    "class": "ResourceRequirement",\n                    "ramMin": 8\n                }\n            ],\n            "id": "#revtool.cwl",\n            "inputs": [\n                {\n                    "id": "#revtool.cwl/input",\n                    "inputBinding": {},\n                    "type": "File"\n                }\n            ],\n            "outputs": [\n                {\n                    "id": "#revtool.cwl/output",\n                    "outputBinding": {\n                        "glob": "output.txt"\n                    },\n                    "type": "File"\n                }\n            ],\n            "stdout": "output.txt"\n        },\n        {\n            "baseCommand": "sort",\n            "class": "CommandLineTool",\n            "doc": "Sort lines using the `sort` command",\n            "hints": [\n                {\n                    "class": "ResourceRequirement",\n                    "ramMin": 8\n                }\n            ],\n            "id": "#sorttool.cwl",\n            "inputs": [\n                {\n                    "id": "#sorttool.cwl/reverse",\n                    "inputBinding": {\n                        "position": 1,\n                        "prefix": "-r"\n                    },\n                    "type": "boolean"\n                },\n                {\n                    "id": "#sorttool.cwl/input",\n                    "inputBinding": {\n                        "position": 2\n                    },\n                    "type": "File"\n                }\n            ],\n            "outputs": [\n                {\n                    "id": "#sorttool.cwl/output",\n                    "outputBinding": {\n                        "glob": "output.txt"\n                    },\n                    "type": "File"\n                }\n            ],\n            "stdout": "output.txt"\n        }\n    ],\n    "cwlVersion": "v1.0"\n}',
                     },
                 },
             });
@@ -126,7 +126,7 @@ describe("run-process-panel-actions", () => {
                 },
                 runtimeConstraints: {
                     API: true,
-                    ram: 1073741824,
+                    ram: 16256 * (1024 * 1024),
                     vcpus: 1,
                 },
                 schedulingParameters: { max_run_time: undefined },
index 3cc6e7131939b884d925153b456eb39ce2a06284..1df982db9ffbce6baad77975fd85190a92da7431 100644 (file)
@@ -85,9 +85,9 @@ export const getWorkflowRunnerSettings = (workflow: WorkflowResource) => {
     if (hints) {
         const resc = hints.find(item => item.class === 'http://arvados.org/cwl#WorkflowRunnerResources') as WorkflowRunnerResources | undefined;
         if (resc) {
-            if (resc.ramMin) { advancedFormValues[RAM_FIELD] = resc.ramMin; }
+            if (resc.ramMin) { advancedFormValues[RAM_FIELD] = resc.ramMin * (1024 * 1024); }
             if (resc.coresMin) { advancedFormValues[VCPUS_FIELD] = resc.coresMin; }
-            if (resc.keep_cache) { advancedFormValues[KEEP_CACHE_RAM_FIELD] = resc.keep_cache; }
+            if (resc.keep_cache) { advancedFormValues[KEEP_CACHE_RAM_FIELD] = resc.keep_cache * (1024 * 1024); }
             if (resc.acrContainerImage) { advancedFormValues[RUNNER_IMAGE_FIELD] = resc.acrContainerImage; }
         }
     }
@@ -158,7 +158,7 @@ export const runProcess = async (dispatch: Dispatch<any>, getState: () => RootSt
             runtimeConstraints: {
                 API: true,
                 vcpus: advancedForm[VCPUS_FIELD],
-                ram: advancedForm[RAM_FIELD],
+                ram: (advancedForm[KEEP_CACHE_RAM_FIELD] + advancedForm[RAM_FIELD]),
             },
             schedulingParameters: {
                 max_run_time: advancedForm[RUNTIME_FIELD]
@@ -189,6 +189,7 @@ export const runProcess = async (dispatch: Dispatch<any>, getState: () => RootSt
 export const DEFAULT_ADVANCED_FORM_VALUES: Partial<RunProcessAdvancedFormData> = {
     [VCPUS_FIELD]: 1,
     [RAM_FIELD]: 1073741824,
+    [KEEP_CACHE_RAM_FIELD]: 268435456,
     [RUNNER_IMAGE_FIELD]: "arvados/jobs"
 };
 
diff --git a/src/views-components/collections-dialog/restore-version-dialog.ts b/src/views-components/collections-dialog/restore-version-dialog.ts
new file mode 100644 (file)
index 0000000..efd7989
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { COLLECTION_RESTORE_VERSION_DIALOG, restoreVersion } from '~/store/collections/collection-version-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(restoreVersion(props.data.uuid));
+    }
+});
+
+export const RestoreCollectionVersionDialog = compose(
+    withDialog(COLLECTION_RESTORE_VERSION_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
\ No newline at end of file
index 4b6b9224df0f0cb5902ef11301e02739c5371be8..afdc03b5f5a43f830ff2027f4a45905af5655cf0 100644 (file)
@@ -2,31 +2,51 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ContextMenuActionSet } from "../context-menu-action-set";
+import {
+    ContextMenuAction,
+    ContextMenuActionSet
+} from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } from "~/components/icon/icon";
+import {
+    RenameIcon,
+    ShareIcon,
+    MoveToIcon,
+    CopyIcon,
+    DetailsIcon,
+    AdvancedIcon,
+    OpenIcon,
+    Link,
+    RestoreVersionIcon,
+    FolderSharedIcon
+} 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 { openWebDavS3InfoDialog } from "~/store/collections/collection-info-actions";
 import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
 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';
 import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
+import { openRestoreCollectionVersionDialog } from "~/store/collections/collection-version-actions";
+import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
+import { togglePublicFavorite } from "~/store/public-favorites/public-favorites-actions";
+import { publicFavoritePanelActions } from "~/store/public-favorites-panel/public-favorites-action";
 
-export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
-    {
-        component: ToggleFavoriteAction,
-        name: 'ToggleFavoriteAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
+const toggleFavoriteAction: ContextMenuAction = {
+    component: ToggleFavoriteAction,
+    name: 'ToggleFavoriteAction',
+    execute: (dispatch, resource) => {
+        dispatch<any>(toggleFavorite(resource)).then(() => {
+            dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+        });
+    }
+};
+
+const commonActionSet: ContextMenuActionSet = [[
     {
         icon: OpenIcon,
         name: "Open in new tab",
@@ -65,6 +85,18 @@ export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
     },
 ]];
 
+export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
+    ...commonActionSet.reduce((prev, next) => prev.concat(next), []),
+    toggleFavoriteAction,
+    {
+        icon: FolderSharedIcon,
+        name: "Open as network folder or S3 bucket",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openWebDavS3InfoDialog(resource.uuid));
+        }
+    },
+]];
+
 export const collectionActionSet: ContextMenuActionSet = [
     [
         ...readOnlyCollectionActionSet.reduce((prev, next) => prev.concat(next), []),
@@ -96,3 +128,31 @@ export const collectionActionSet: ContextMenuActionSet = [
         },
     ]
 ];
+
+export const collectionAdminActionSet: ContextMenuActionSet = [
+    [
+        ...collectionActionSet.reduce((prev, next) => prev.concat(next), []),
+        {
+            component: TogglePublicFavoriteAction,
+            name: 'TogglePublicFavoriteAction',
+            execute: (dispatch, resource) => {
+                dispatch<any>(togglePublicFavorite(resource)).then(() => {
+                    dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
+                });
+            }
+        },
+    ]
+];
+
+export const oldCollectionVersionActionSet: ContextMenuActionSet = [
+    [
+        ...commonActionSet.reduce((prev, next) => prev.concat(next), []),
+        {
+            icon: RestoreVersionIcon,
+            name: 'Restore version',
+            execute: (dispatch, { uuid }) => {
+                dispatch<any>(openRestoreCollectionVersionDialog(uuid));
+            }
+        },
+    ]
+];
diff --git a/src/views-components/context-menu/action-sets/collection-admin-action-set.ts b/src/views-components/context-menu/action-sets/collection-admin-action-set.ts
deleted file mode 100644 (file)
index 7b39d74..0000000
+++ /dev/null
@@ -1,104 +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 { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } 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 { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
-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';
-import { TogglePublicFavoriteAction } from "~/views-components/context-menu/actions/public-favorite-action";
-import { publicFavoritePanelActions } from "~/store/public-favorites-panel/public-favorites-action";
-import { togglePublicFavorite } from "~/store/public-favorites/public-favorites-actions";
-import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
-
-export const collectionAdminActionSet: ContextMenuActionSet = [[
-    {
-        icon: RenameIcon,
-        name: "Edit collection",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openCollectionUpdateDialog(resource));
-        }
-    },
-    {
-        icon: OpenIcon,
-        name: "Open in new tab",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openInNewTabAction(resource));
-        }
-    },
-    {
-        icon: Link,
-        name: "Copy to clipboard",
-        execute: (dispatch, resource) => {
-            dispatch<any>(copyToClipboardAction(resource));
-        }
-    },
-    {
-        icon: ShareIcon,
-        name: "Share",
-        execute: (dispatch, { uuid }) => {
-            dispatch<any>(openSharingDialog(uuid));
-        }
-    },
-    {
-        component: ToggleFavoriteAction,
-        name: 'ToggleFavoriteAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
-    {
-        component: TogglePublicFavoriteAction,
-        name: 'TogglePublicFavoriteAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(togglePublicFavorite(resource)).then(() => {
-                dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
-    {
-        icon: MoveToIcon,
-        name: "Move to",
-        execute: (dispatch, resource) => dispatch<any>(openMoveCollectionDialog(resource))
-    },
-    {
-        icon: CopyIcon,
-        name: "Make a copy",
-        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,
-        name: 'ToggleTrashAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
-        }
-    },
-]];
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 5bd362f..0000000
+++ /dev/null
@@ -1,91 +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, OpenIcon } 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';
-import { openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
-
-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: OpenIcon,
-        name: "Open in new tab",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openInNewTabAction(resource));
-        }
-    },
-    {
-        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 c0b925c2ce49eab712926577a6abfe169eff38dd..57ba0ea3f1fcbcf98c9a763878ff6655a8a77a57 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } from '~/components/icon/icon';
+import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link, FolderSharedIcon } from '~/components/icon/icon';
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
@@ -17,6 +17,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';
 import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
+import { openWebDavS3InfoDialog } from "~/store/collections/collection-info-actions";
 
 export const readOnlyProjectActionSet: ContextMenuActionSet = [[
     {
@@ -56,6 +57,13 @@ export const readOnlyProjectActionSet: ContextMenuActionSet = [[
             dispatch<any>(openAdvancedTabDialog(resource.uuid));
         }
     },
+    {
+        icon: FolderSharedIcon,
+        name: "Open as network folder or S3 bucket",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openWebDavS3InfoDialog(resource.uuid));
+        }
+    },
 ]];
 
 export const projectActionSet: ContextMenuActionSet = [
index 398864dc97b7b5bc4147fd3639dfaab8d9da54d5..a3a8ce79e9ffd897c91dcb3751660abff71d4d54 100644 (file)
@@ -3,69 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link } from '~/components/icon/icon';
-import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { toggleFavorite } from "~/store/favorites/favorites-actions";
-import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
-import { openMoveProjectDialog } from '~/store/projects/project-move-actions';
-import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
-import { openProjectUpdateDialog } from '~/store/projects/project-update-actions';
-import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
-import { toggleProjectTrashed } from "~/store/trash/trash-actions";
-import { ShareIcon } from '~/components/icon/icon';
-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';
 import { TogglePublicFavoriteAction } from "~/views-components/context-menu/actions/public-favorite-action";
 import { togglePublicFavorite } from "~/store/public-favorites/public-favorites-actions";
 import { publicFavoritePanelActions } from "~/store/public-favorites-panel/public-favorites-action";
-import { copyToClipboardAction, openInNewTabAction } from "~/store/open-in-new-tab/open-in-new-tab.actions";
+
+import { projectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
 
 export const projectAdminActionSet: ContextMenuActionSet = [[
-    {
-        icon: NewProjectIcon,
-        name: "New project",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProjectCreateDialog(resource.uuid));
-        }
-    },
-    {
-        icon: OpenIcon,
-        name: "Open in new tab",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openInNewTabAction(resource));
-        }
-    },
-    {
-        icon: Link,
-        name: "Copy to clipboard",
-        execute: (dispatch, resource) => {
-            dispatch<any>(copyToClipboardAction(resource));
-        }
-    },
-    {
-        icon: RenameIcon,
-        name: "Edit project",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProjectUpdateDialog(resource));
-        }
-    },
-    {
-        icon: ShareIcon,
-        name: "Share",
-        execute: (dispatch, { uuid }) => {
-            dispatch<any>(openSharingDialog(uuid));
-        }
-    },
-    {
-        component: ToggleFavoriteAction,
-        name: 'ToggleFavoriteAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
+    ...projectActionSet.reduce((prev, next) => prev.concat(next), []),
     {
         component: TogglePublicFavoriteAction,
         name: 'TogglePublicFavoriteAction',
@@ -74,40 +19,5 @@ export const projectAdminActionSet: ContextMenuActionSet = [[
                 dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
             });
         }
-    },
-    {
-        icon: MoveToIcon,
-        name: "Move to",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openMoveProjectDialog(resource));
-        }
-    },
-    // {
-    //     icon: CopyIcon,
-    //     name: "Copy to project",
-    //     execute: (dispatch, resource) => {
-    //         // add code
-    //     }
-    // },
-    {
-        icon: DetailsIcon,
-        name: "View details",
-        execute: dispatch => {
-            dispatch<any>(toggleDetailsPanel());
-        }
-    },
-    {
-        icon: AdvancedIcon,
-        name: "Advanced",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openAdvancedTabDialog(resource.uuid));
-        }
-    },
-    {
-        component: ToggleTrashAction,
-        name: 'ToggleTrashAction',
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
-        }
-    },
+    }
 ]];
index b86498a0e9687dfa7fce6e2e2306ee263569ef30..219913cdd13ce4a2549779159a8ab8f62a4be9c7 100644 (file)
@@ -80,6 +80,7 @@ export enum ContextMenuKind {
     COLLECTION = 'Collection',
     COLLECTION_ADMIN = 'CollectionAdmin',
     READONLY_COLLECTION = 'ReadOnlyCollection',
+    OLD_VERSION_COLLECTION = 'OldVersionCollection',
     TRASHED_COLLECTION = 'TrashedCollection',
     PROCESS = "Process",
     PROCESS_ADMIN = 'ProcessAdmin',
index 6066a5889fa7508921e83759af835d4f7ca79b13..f5bac3662c7b96668df47a60ee5d5d1174ad3525 100644 (file)
@@ -14,8 +14,7 @@ import { Grid, ListItem, StyleRulesCallback, Typography, withStyles, WithStyles
 import { formatDate, formatFileSize } from '~/common/formatters';
 import { Dispatch } from 'redux';
 import { navigateTo } from '~/store/navigation/navigation-action';
-import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { ContextMenuKind } from '../context-menu/context-menu';
+import { openContextMenu, resourceUuidToContextMenuKind } from '~/store/context-menu/context-menu-actions';
 
 export type CssRules = 'versionBrowserHeader' | 'versionBrowserItem';
 
@@ -62,29 +61,28 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
 interface CollectionVersionBrowserProps {
     currentCollection: CollectionResource | undefined;
     versions: CollectionResource[];
-    isAdmin: boolean;
 }
 
 interface CollectionVersionBrowserDispatchProps {
     showVersion: (c: CollectionResource) => void;
-    handleContextMenu: (event: React.MouseEvent<HTMLElement>, collection: CollectionResource, menuKind: ContextMenuKind | undefined) => void;
+    handleContextMenu: (event: React.MouseEvent<HTMLElement>, collection: CollectionResource) => void;
 }
 
 const mapStateToProps = (state: RootState): CollectionVersionBrowserProps => {
     const currentCollection = getResource<CollectionResource>(state.detailsPanel.resourceUuid)(state.resources);
-    const isAdmin = state.auth.user!.isAdmin;
     const versions = currentCollection
         && filterResources(rsc =>
             (rsc as CollectionResource).currentVersionUuid === currentCollection.currentVersionUuid)(state.resources)
                 .sort((a: CollectionResource, b: CollectionResource) => b.version - a.version) as CollectionResource[]
         || [];
-    return { currentCollection, versions, isAdmin };
+    return { currentCollection, versions };
 };
 
 const mapDispatchToProps = () =>
     (dispatch: Dispatch): CollectionVersionBrowserDispatchProps => ({
         showVersion: (collection) => dispatch<any>(navigateTo(collection.uuid)),
-        handleContextMenu: (event: React.MouseEvent<HTMLElement>, collection: CollectionResource, menuKind: ContextMenuKind) => {
+        handleContextMenu: (event: React.MouseEvent<HTMLElement>, collection: CollectionResource) => {
+            const menuKind = dispatch<any>(resourceUuidToContextMenuKind(collection.uuid));
             if (collection && menuKind) {
                 dispatch<any>(openContextMenu(event, {
                     name: collection.name,
@@ -100,7 +98,7 @@ const mapDispatchToProps = () =>
 
 const CollectionVersionBrowser = withStyles(styles)(
     connect(mapStateToProps, mapDispatchToProps)(
-        ({ currentCollection, versions, isAdmin, showVersion, handleContextMenu, classes }: CollectionVersionBrowserProps & CollectionVersionBrowserDispatchProps & WithStyles<CssRules>) => {
+        ({ currentCollection, versions, showVersion, handleContextMenu, classes }: CollectionVersionBrowserProps & CollectionVersionBrowserDispatchProps & WithStyles<CssRules>) => {
             return <div data-cy="collection-version-browser">
                 <Grid container>
                     <Grid item xs={2}>
@@ -125,14 +123,7 @@ const CollectionVersionBrowser = withStyles(styles)(
                             data-cy={`collection-version-browser-select-${item.version}`}
                             key={item.version}
                             onClick={e => showVersion(item)}
-                            onContextMenu={event => handleContextMenu(
-                                event,
-                                item,
-                                resourceKindToContextMenuKind(
-                                    item.uuid,
-                                    isAdmin,
-                                    (item.uuid === item.currentVersionUuid))
-                            )}
+                            onContextMenu={event => handleContextMenu(event, item)}
                             selected={isSelectedVersion}>
                             <Grid item xs={2}>
                                 <Typography variant="caption" className={classes.versionBrowserItem}>
diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.test.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.test.tsx
new file mode 100644 (file)
index 0000000..dab324b
--- /dev/null
@@ -0,0 +1,135 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { mount, configure, shallow } from 'enzyme';
+import * as Adapter from "enzyme-adapter-react-16";
+import { MuiThemeProvider, WithStyles } from '@material-ui/core';
+import { CustomTheme } from '~/common/custom-theme';
+import { WebDavS3InfoDialog, CssRules } from './webdav-s3-dialog';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { WebDavS3InfoDialogData, COLLECTION_WEBDAV_S3_DIALOG_NAME } from '~/store/collections/collection-info-actions';
+import { Provider } from "react-redux";
+import { createStore, combineReducers } from 'redux';
+import { configureStore, RootStore } from '~/store/store';
+import { createBrowserHistory } from "history";
+import { createServices } from "~/services/services";
+
+configure({ adapter: new Adapter() });
+
+describe('WebDavS3InfoDialog', () => {
+    let props: WithDialogProps<WebDavS3InfoDialogData> & WithStyles<CssRules>;
+    let store;
+
+    beforeEach(() => {
+        const initialDialogState = {
+            [COLLECTION_WEBDAV_S3_DIALOG_NAME]: {
+                open: true,
+                data: {
+                    uuid: "zzzzz-4zz18-b1f8tbldjrm8885",
+                    token: "v2/zzzzb-jjjjj-123123/xxxtokenxxx",
+                    downloadUrl: "https://download.example.com",
+                    collectionsUrl: "https://collections.example.com",
+                    localCluster: "zzzzz",
+                    username: "bobby",
+                    activeTab: 0,
+                    setActiveTab: (event: any, tabNr: number) => { }
+                }
+            }
+        };
+        const initialAuthState = {
+            localCluster: "zzzzz",
+            remoteHostsConfig: {},
+            sessions: {},
+        };
+        store = createStore(combineReducers({
+            dialog: (state: any = initialDialogState, action: any) => state,
+            auth: (state: any = initialAuthState, action: any) => state,
+        }));
+
+        props = {
+            classes: {
+                details: 'details',
+            }
+        };
+    });
+
+    it('render cyberduck tab', () => {
+        store.getState().dialog[COLLECTION_WEBDAV_S3_DIALOG_NAME].data.activeTab = 0;
+        // when
+        const wrapper = mount(
+            <MuiThemeProvider theme={CustomTheme}>
+                <Provider store={store}>
+                    <WebDavS3InfoDialog {...props} />
+                </Provider>
+            </MuiThemeProvider>
+        );
+
+        // then
+        expect(wrapper.text()).toContain("davs://bobby@download.example.com/by_id/zzzzz-4zz18-b1f8tbldjrm8885");
+    });
+
+    it('render win/mac tab', () => {
+        store.getState().dialog[COLLECTION_WEBDAV_S3_DIALOG_NAME].data.activeTab = 1;
+        // when
+        const wrapper = mount(
+            <MuiThemeProvider theme={CustomTheme}>
+                <Provider store={store}>
+                    <WebDavS3InfoDialog {...props} />
+                </Provider>
+            </MuiThemeProvider>
+        );
+
+        // then
+        expect(wrapper.text()).toContain("https://download.example.com/by_id/zzzzz-4zz18-b1f8tbldjrm8885");
+    });
+
+    it('render s3 tab with federated token', () => {
+        store.getState().dialog[COLLECTION_WEBDAV_S3_DIALOG_NAME].data.activeTab = 2;
+        // when
+        const wrapper = mount(
+            <MuiThemeProvider theme={CustomTheme}>
+                <Provider store={store}>
+                    <WebDavS3InfoDialog {...props} />
+                </Provider>
+            </MuiThemeProvider>
+        );
+
+        // then
+        expect(wrapper.text()).toContain("Secret Keyv2_zzzzb-jjjjj-123123_xxxtokenxxx");
+    });
+
+    it('render s3 tab with local token', () => {
+        store.getState().dialog[COLLECTION_WEBDAV_S3_DIALOG_NAME].data.activeTab = 2;
+        store.getState().dialog[COLLECTION_WEBDAV_S3_DIALOG_NAME].data.token = "v2/zzzzz-jjjjj-123123/xxxtokenxxx";
+        // when
+        const wrapper = mount(
+            <MuiThemeProvider theme={CustomTheme}>
+                <Provider store={store}>
+                    <WebDavS3InfoDialog {...props} />
+                </Provider>
+            </MuiThemeProvider>
+        );
+
+        // then
+        expect(wrapper.text()).toContain("Access Keyzzzzz-jjjjj-123123Secret Keyxxxtokenxxx");
+    });
+
+    it('render cyberduck tab with wildcard DNS', () => {
+        store.getState().dialog[COLLECTION_WEBDAV_S3_DIALOG_NAME].data.activeTab = 0;
+        store.getState().dialog[COLLECTION_WEBDAV_S3_DIALOG_NAME].data.collectionsUrl = "https://*.collections.example.com";
+        // when
+        const wrapper = mount(
+            <MuiThemeProvider theme={CustomTheme}>
+                <Provider store={store}>
+                    <WebDavS3InfoDialog {...props} />
+                </Provider>
+            </MuiThemeProvider>
+        );
+
+        // then
+        expect(wrapper.text()).toContain("davs://bobby@zzzzz-4zz18-b1f8tbldjrm8885.collections.example.com");
+    });
+
+});
diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
new file mode 100644 (file)
index 0000000..018a0ef
--- /dev/null
@@ -0,0 +1,193 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogActions, Button, StyleRulesCallback, WithStyles, withStyles, CardHeader, Tab, Tabs } from '@material-ui/core';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { COLLECTION_WEBDAV_S3_DIALOG_NAME, WebDavS3InfoDialogData } from '~/store/collections/collection-info-actions';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { compose } from 'redux';
+import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
+
+export type CssRules = 'details';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+    details: {
+        marginLeft: theme.spacing.unit * 3,
+        marginRight: theme.spacing.unit * 3,
+    }
+});
+
+interface TabPanelData {
+    children: React.ReactElement<any>[];
+    value: number;
+    index: number;
+}
+
+function TabPanel(props: TabPanelData) {
+    const { children, value, index } = props;
+
+    return (
+        <div
+            role="tabpanel"
+            hidden={value !== index}
+            id={`simple-tabpanel-${index}`}
+            aria-labelledby={`simple-tab-${index}`}
+        >
+            {value === index && children}
+        </div>
+    );
+}
+
+export const WebDavS3InfoDialog = compose(
+    withDialog(COLLECTION_WEBDAV_S3_DIALOG_NAME),
+    withStyles(styles),
+)(
+    (props: WithDialogProps<WebDavS3InfoDialogData> & WithStyles<CssRules>) => {
+        if (!props.data.downloadUrl) { return null; }
+
+        let winDav;
+        let cyberDav;
+
+        if (props.data.collectionsUrl.indexOf("*") > -1) {
+            const withuuid = props.data.collectionsUrl.replace("*", props.data.uuid);
+            winDav = new URL(withuuid);
+            cyberDav = new URL(withuuid);
+        } else {
+            winDav = new URL(props.data.downloadUrl);
+            cyberDav = new URL(props.data.downloadUrl);
+            winDav.pathname = `/by_id/${props.data.uuid}`;
+            cyberDav.pathname = `/by_id/${props.data.uuid}`;
+        }
+
+        cyberDav.username = props.data.username;
+        const cyberDavStr = "dav" + cyberDav.toString().slice(4);
+
+        const s3endpoint = new URL(props.data.collectionsUrl.replace(/\/\*(--[^.]+)?\./, "/"));
+
+        const sp = props.data.token.split("/");
+        let tokenUuid;
+        let tokenSecret;
+        if (sp.length === 3 && sp[0] === "v2" && sp[1].slice(0, 5) === props.data.localCluster) {
+            tokenUuid = sp[1];
+            tokenSecret = sp[2];
+        } else {
+            tokenUuid = props.data.token.replace(/\//g, "_");
+            tokenSecret = tokenUuid;
+        }
+
+        const supportsWebdav = (props.data.uuid.indexOf("-4zz18-") === 5);
+
+        let activeTab = props.data.activeTab;
+        if (!supportsWebdav) {
+            activeTab = 2;
+        }
+
+        return <Dialog
+            open={props.open}
+            maxWidth="md"
+            onClose={props.closeDialog}
+            style={{ alignSelf: 'stretch' }}>
+            <CardHeader
+                title={`Open as Network Folder or S3 Bucket`} />
+            <div className={props.classes.details} >
+                <Tabs value={activeTab} onChange={props.data.setActiveTab}>
+                    {supportsWebdav && <Tab value={0} key="cyberduck" label="Cyberduck/Mountain Duck or Gnome Files" />}
+                    {supportsWebdav && <Tab value={1} key="windows" label="Windows or MacOS" />}
+                    <Tab value={2} key="s3" label="S3 bucket" />
+                </Tabs>
+
+                <TabPanel index={1} value={activeTab}>
+                    <h2>Settings</h2>
+
+                    <DetailsAttribute
+                        label='Internet address'
+                        value={<a href={winDav.toString()} target="_blank">{winDav.toString()}</a>}
+                        copyValue={winDav.toString()} />
+
+                    <DetailsAttribute
+                        label='Username'
+                        value={props.data.username}
+                        copyValue={props.data.username} />
+
+                    <DetailsAttribute
+                        label='Password'
+                        value={props.data.token}
+                        copyValue={props.data.token} />
+
+                    <h3>Windows</h3>
+                    <ol>
+                        <li>Open File Explorer</li>
+                        <li>Click on "This PC", then go to Computer &rarr; Add a Network Location</li>
+                        <li>Click Next, then choose "Add a custom network location", then click Next</li>
+                    </ol>
+
+                    <h3>MacOS</h3>
+                    <ol>
+                        <li>Open Finder</li>
+                        <li>Click Go &rarr; Connect to server</li>
+                    </ol>
+                </TabPanel>
+
+                <TabPanel index={0} value={activeTab}>
+                    <DetailsAttribute
+                        label='Server'
+                        value={<a href={cyberDavStr} target="_blank">{cyberDavStr}</a>}
+                        copyValue={cyberDavStr} />
+
+                    <DetailsAttribute
+                        label='Username'
+                        value={props.data.username}
+                        copyValue={props.data.username} />
+
+                    <DetailsAttribute
+                        label='Password'
+                        value={props.data.token}
+                        copyValue={props.data.token} />
+
+                    <h3>Gnome</h3>
+                    <ol>
+                        <li>Open Files</li>
+                        <li>Select +Other Locations</li>
+                        <li>Connect to Server &rarr; Enter server address</li>
+                    </ol>
+
+                </TabPanel>
+
+                <TabPanel index={2} value={activeTab}>
+                    <DetailsAttribute
+                        label='Endpoint'
+                        value={s3endpoint.host}
+                        copyValue={s3endpoint.host} />
+
+                    <DetailsAttribute
+                        label='Bucket'
+                        value={props.data.uuid}
+                        copyValue={props.data.uuid} />
+
+                    <DetailsAttribute
+                        label='Access Key'
+                        value={tokenUuid}
+                        copyValue={tokenUuid} />
+
+                    <DetailsAttribute
+                        label='Secret Key'
+                        value={tokenSecret}
+                        copyValue={tokenSecret} />
+
+                </TabPanel>
+
+            </div>
+            <DialogActions>
+                <Button
+                    variant='text'
+                    color='primary'
+                    onClick={props.closeDialog}>
+                    Close
+               </Button>
+            </DialogActions>
+
+        </Dialog >;
+    }
+);
index 038fea2fd44eb4bb3de3d060a3cf163fedd9b6bb..06ea910d9cda3d99d3458257e1ad978dbdd5f4fc 100644 (file)
@@ -3,7 +3,13 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles, Grid, Button } from '@material-ui/core';
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    Grid,
+    Button
+} from '@material-ui/core';
 import { CollectionIcon } from '~/components/icon/icon';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { BackIcon } from '~/components/icon/icon';
@@ -11,8 +17,10 @@ import { DataTableDefaultView } from '~/components/data-table-default-view/data-
 import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { Dispatch } from 'redux';
-import { getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
-import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import {
+    resourceUuidToContextMenuKind,
+    openContextMenu
+} from '~/store/context-menu/context-menu-actions';
 import { ResourceKind } from '~/models/resource';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { connect } from 'react-redux';
@@ -20,7 +28,12 @@ import { navigateTo } from '~/store/navigation/navigation-action';
 import { DataColumns } from '~/components/data-table/data-table';
 import { SortDirection } from '~/components/data-table/data-column';
 import { createTree } from '~/models/tree';
-import { ResourceName, ResourceOwnerName, ResourceLastModifiedDate, ResourceStatus } from '~/views-components/data-explorer/renderers';
+import {
+    ResourceName,
+    ResourceOwnerName,
+    ResourceLastModifiedDate,
+    ResourceStatus
+} from '~/views-components/data-explorer/renderers';
 
 type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
 
@@ -105,8 +118,7 @@ export interface CollectionContentAddressPanelActionProps {
 
 const mapDispatchToProps = (dispatch: Dispatch): CollectionContentAddressPanelActionProps => ({
     onContextMenu: (event, resourceUuid) => {
-        const isAdmin = dispatch<any>(getIsAdmin());
-        const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin);
+        const kind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
         if (kind) {
             dispatch<any>(openContextMenu(event, {
                 name: '',
index b7bd3a62a5cfd54dda1ff6490b84cce37bb517db..685bb78bda561cd90392afc9eecca7078e7b0784 100644 (file)
@@ -19,8 +19,7 @@ import { CollectionPanelFiles } from '~/views-components/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 { openContextMenu, resourceUuidToContextMenuKind } from '~/store/context-menu/context-menu-actions';
 import { formatDate, formatFileSize } from "~/common/formatters";
 import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
@@ -237,19 +236,15 @@ export const CollectionPanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<any>) => {
-                const { uuid, ownerUuid, name, description, kind, isTrashed } = this.props.item;
-                const { isWritable } = this.props;
+                const { uuid, ownerUuid, name, description, kind } = this.props.item;
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(uuid));
                 const resource = {
                     uuid,
                     ownerUuid,
                     name,
                     description,
                     kind,
-                    menuKind: isWritable
-                        ? isTrashed
-                            ? ContextMenuKind.TRASHED_COLLECTION
-                            : ContextMenuKind.COLLECTION
-                        : ContextMenuKind.READONLY_COLLECTION
+                    menuKind,
                 };
                 // Avoid expanding/collapsing the panel
                 event.stopPropagation();
index cad2f9ba5b42d3c8f84c5df0db5b6d97694bf7be..48a9e33029d679ec90a4ebe4d35ee966c7cbfad8 100644 (file)
@@ -10,7 +10,7 @@ import { DataColumns } from '~/components/data-table/data-table';
 import { RouteComponentProps } from 'react-router';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { SortDirection } from '~/components/data-table/data-column';
-import { ResourceKind, EditableResource } from '~/models/resource';
+import { ResourceKind } from '~/models/resource';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
 import {
@@ -22,7 +22,10 @@ import {
     ResourceType
 } from '~/views-components/data-explorer/renderers';
 import { FavoriteIcon } from '~/components/icon/icon';
-import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { ContainerRequestState } from "~/models/container-request";
@@ -31,8 +34,7 @@ import { RootState } from '~/store/store';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { createTree } from '~/models/tree';
 import { getSimpleObjectTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
-import { getResourceWithEditableStatus, ResourcesState } from '~/store/resources/resources';
-import { ProjectResource } from '~/models/project';
+import { ResourcesState } from '~/store/resources/resources';
 
 type CssRules = "toolbar" | "button";
 
@@ -109,7 +111,6 @@ export const favoritePanelColumns: DataColumns<string> = [
 interface FavoritePanelDataProps {
     favorites: FavoritesState;
     resources: ResourcesState;
-    isAdmin: boolean;
     userUuid: string;
 }
 
@@ -121,7 +122,6 @@ interface FavoritePanelActionProps {
 const mapStateToProps = (state : RootState): FavoritePanelDataProps => ({
     favorites: state.favorites,
     resources: state.resources,
-    isAdmin: state.auth.user!.isAdmin,
     userUuid: state.auth.user!.uuid,
 });
 
@@ -133,9 +133,7 @@ export const FavoritePanel = withStyles(styles)(
         class extends React.Component<FavoritePanelProps> {
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const { isAdmin, userUuid, resources } = this.props;
-                const resource = getResourceWithEditableStatus<ProjectResource & EditableResource>(resourceUuid, userUuid)(resources);
-                const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable);
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
                 if (menuKind) {
                     this.props.dispatch<any>(openContextMenu(event, {
                         name: '',
index 4bff4ee7c6be609cba72092cb0a2d43c153e5685..f9ec763bee0804a536ba6804e028817f2e8b82b6 100644 (file)
@@ -5,8 +5,15 @@
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
 import { RootState } from '~/store/store';
-import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
-import { LinkPanelRoot, LinkPanelRootActionProps, LinkPanelRootDataProps } from '~/views/link-panel/link-panel-root';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
+import {
+    LinkPanelRoot,
+    LinkPanelRootActionProps,
+    LinkPanelRootDataProps
+} from '~/views/link-panel/link-panel-root';
 import { ResourceKind } from '~/models/resource';
 
 const mapStateToProps = (state: RootState): LinkPanelRootDataProps => {
@@ -17,7 +24,7 @@ const mapStateToProps = (state: RootState): LinkPanelRootDataProps => {
 
 const mapDispatchToProps = (dispatch: Dispatch): LinkPanelRootActionProps => ({
     onContextMenu: (event, resourceUuid) => {
-        const kind = resourceKindToContextMenuKind(resourceUuid);
+        const kind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
         if (kind) {
             dispatch<any>(openContextMenu(event, {
                 name: '',
index acaa43ad2b8b67741ae6babf238d65f38cf832fa..8f41ff3d96d1965ca1a5f1e07a71714ac3e30a9a 100644 (file)
@@ -48,7 +48,7 @@ export const MainPanelRoot = withStyles(styles)(
                     uuidPrefix={uuidPrefix}
                     siteBanner={siteBanner}>
                     {working
-                        ? <LinearProgress color="secondary" />
+                        ? <LinearProgress color="secondary" data-cy="linear-progress" />
                         : null}
                 </MainAppBar>}
                 <Grid container direction="column" className={classes.root}>
index 11223f225ec2e6a1bddb08175086e363149dc655..47dbd9b062b665f0c92f08a2d0079bb443833a0f 100644 (file)
@@ -14,7 +14,7 @@ import { RootState } from '~/store/store';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { ContainerRequestState } from '~/models/container-request';
 import { SortDirection } from '~/components/data-table/data-column';
-import { ResourceKind, Resource, EditableResource } from '~/models/resource';
+import { ResourceKind, Resource } from '~/models/resource';
 import {
     ResourceFileSize,
     ResourceLastModifiedDate,
@@ -24,17 +24,26 @@ import {
 } from '~/views-components/data-explorer/renderers';
 import { ProjectIcon } from '~/components/icon/icon';
 import { ResourceName } from '~/views-components/data-explorer/renderers';
-import { ResourcesState, getResourceWithEditableStatus } from '~/store/resources/resources';
+import {
+    ResourcesState,
+    getResource
+} from '~/store/resources/resources';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { ProjectResource } from '~/models/project';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { getProperty } from '~/store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { ArvadosTheme } from "~/common/custom-theme";
 import { createTree } from '~/models/tree';
-import { getInitialResourceTypeFilters, getInitialProcessStatusFilters } from '~/store/resource-type-filters/resource-type-filters';
+import {
+    getInitialResourceTypeFilters,
+    getInitialProcessStatusFilters
+} from '~/store/resource-type-filters/resource-type-filters';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
 
 type CssRules = 'root' | "button";
 
@@ -131,7 +140,6 @@ export const ProjectPanel = withStyles(styles)(
     connect((state: RootState) => ({
         currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
         resources: state.resources,
-        isAdmin: state.auth.user!.isAdmin,
         userUuid: state.auth.user!.uuid,
     }))(
         class extends React.Component<ProjectPanelProps> {
@@ -157,15 +165,15 @@ export const ProjectPanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const { isAdmin, userUuid, resources } = this.props;
-                const resource = getResourceWithEditableStatus<ProjectResource & EditableResource>(resourceUuid, userUuid)(resources);
-                const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable);
+                const { resources } = this.props;
+                const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
                 if (menuKind && resource) {
                     this.props.dispatch<any>(openContextMenu(event, {
                         name: resource.name,
                         uuid: resource.uuid,
                         ownerUuid: resource.ownerUuid,
-                        isTrashed: resource.isTrashed,
+                        isTrashed: ('isTrashed' in resource) ? resource.isTrashed: false,
                         kind: resource.kind,
                         menuKind
                     }));
index 635ac6213c2122c1be60973525ea0e912481c3f3..800e5e599fcedff1ec7578106fc3f74a9c8f688e 100644 (file)
@@ -22,7 +22,10 @@ import {
 } from '~/views-components/data-explorer/renderers';
 import { PublicFavoriteIcon } from '~/components/icon/icon';
 import { Dispatch } from 'redux';
-import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { ContainerRequestState } from "~/models/container-request";
@@ -32,7 +35,6 @@ import { createTree } from '~/models/tree';
 import { getSimpleObjectTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
 import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
 import { PublicFavoritesState } from '~/store/public-favorites/public-favorites-reducer';
-import { getIsAdmin } from '~/store/public-favorites/public-favorites-actions';
 
 type CssRules = "toolbar" | "button";
 
@@ -122,8 +124,7 @@ const mapStateToProps = ({ publicFavorites }: RootState): PublicFavoritePanelDat
 
 const mapDispatchToProps = (dispatch: Dispatch): PublicFavoritePanelActionProps => ({
     onContextMenu: (event, resourceUuid) => {
-        const isAdmin = dispatch<any>(getIsAdmin());
-        const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin);
+        const kind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
         if (kind) {
             dispatch<any>(openContextMenu(event, {
                 name: '',
index d9848291c98431abf8b67dec98a461146971abd3..71f75cf55b7d8106fdf8a06f605cb1a0910b3c12 100644 (file)
@@ -26,7 +26,7 @@ export interface RunProcessAdvancedFormData {
     [RUNTIME_FIELD]?: number;
     [RAM_FIELD]: number;
     [VCPUS_FIELD]: number;
-    [KEEP_CACHE_RAM_FIELD]?: number;
+    [KEEP_CACHE_RAM_FIELD]: number;
     [RUNNER_IMAGE_FIELD]: string;
 }
 
index 9b4bcc8572d890d53d0f62abdfea6f0890e286c5..76a314ae306bd41ff9f56b6118e5c8ba7f45bac8 100644 (file)
@@ -9,14 +9,16 @@ import { connect, DispatchProp } from 'react-redux';
 import { RootState } from '~/store/store';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { ShareMeIcon } from '~/components/icon/icon';
-import { ResourcesState, getResourceWithEditableStatus } from '~/store/resources/resources';
+import { ResourcesState, getResource } from '~/store/resources/resources';
 import { navigateTo } from "~/store/navigation/navigation-action";
 import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { SHARED_WITH_ME_PANEL_ID } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
-import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
-import { GroupResource } from '~/models/group';
-import { EditableResource } from '~/models/resource';
+import {
+    openContextMenu,
+    resourceUuidToContextMenuKind
+} from '~/store/context-menu/context-menu-actions';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
 
 type CssRules = "toolbar" | "button";
 
@@ -32,7 +34,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 interface SharedWithMePanelDataProps {
     resources: ResourcesState;
-    isAdmin: boolean;
     userUuid: string;
 }
 
@@ -41,7 +42,6 @@ type SharedWithMePanelProps = SharedWithMePanelDataProps & DispatchProp & WithSt
 export const SharedWithMePanel = withStyles(styles)(
     connect((state: RootState) => ({
         resources: state.resources,
-        isAdmin: state.auth.user!.isAdmin,
         userUuid: state.auth.user!.uuid,
     }))(
         class extends React.Component<SharedWithMePanelProps> {
@@ -56,15 +56,15 @@ export const SharedWithMePanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const { isAdmin, userUuid, resources } = this.props;
-                const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(resourceUuid, userUuid)(resources);
-                const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable);
+                const { resources } = this.props;
+                const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
                 if (menuKind && resource) {
                     this.props.dispatch<any>(openContextMenu(event, {
                         name: '',
                         uuid: resource.uuid,
                         ownerUuid: resource.ownerUuid,
-                        isTrashed: resource.isTrashed,
+                        isTrashed: ('isTrashed' in resource) ? resource.isTrashed: false,
                         kind: resource.kind,
                         menuKind
                     }));
index 49d880e5e2d87df537b10b5dc704132e8f8436de..1762633a5840daf942c9c8926793c0917889d679 100644 (file)
@@ -61,7 +61,7 @@ const mapStateToProps = (state: RootState) => {
         userUuid: state.auth.user!.uuid,
         helpText: state.auth.config.clusterConfig.Workbench.SSHHelpPageHTML,
         hostSuffix: state.auth.config.clusterConfig.Workbench.SSHHelpHostSuffix || "",
-        webShell: state.auth.config.clusterConfig.Services.WebShell.ExternalURL,
+        webShell: state.auth.config.clusterConfig.Services.Workbench1.ExternalURL,
         ...state.virtualMachines
     };
 };
index b0f90894a91ed4bd68b514061b2c21df60ee07b0..f5cfda89828e8164c758061ec6c41b2a99c94ea6 100644 (file)
@@ -99,6 +99,8 @@ import { CollectionsContentAddressPanel } from '~/views/collection-content-addre
 import { AllProcessesPanel } from '../all-processes-panel/all-processes-panel';
 import { NotFoundPanel } from '../not-found-panel/not-found-panel';
 import { AutoLogout } from '~/views-components/auto-logout/auto-logout';
+import { RestoreCollectionVersionDialog } from '~/views-components/collections-dialog/restore-version-dialog';
+import { WebDavS3InfoDialog } from '~/views-components/webdav-s3-dialog/webdav-s3-dialog';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -150,18 +152,18 @@ const saveSplitterSize = (size: number) => localStorage.setItem('splitterSize',
 export const WorkbenchPanel =
     withStyles(styles)((props: WorkbenchPanelProps) =>
         <Grid container item xs className={props.classes.root}>
-            { props.sessionIdleTimeout > 0 && <AutoLogout />}
+            {props.sessionIdleTimeout > 0 && <AutoLogout />}
             <Grid container item xs className={props.classes.container}>
                 <SplitterLayout customClassName={props.classes.splitter} percentage={true}
                     primaryIndex={0} primaryMinSize={10}
                     secondaryInitialSize={getSplitterInitialSize()} secondaryMinSize={40}
                     onSecondaryPaneSizeChange={saveSplitterSize}>
-                    { props.isUserActive && props.isNotLinking && <Grid container item xs component='aside' direction='column' className={props.classes.asidePanel}>
+                    {props.isUserActive && props.isNotLinking && <Grid container item xs component='aside' direction='column' className={props.classes.asidePanel}>
                         <SidePanel />
-                    </Grid> }
+                    </Grid>}
                     <Grid container item xs component="main" direction="column" className={props.classes.contentWrapper}>
                         <Grid item xs>
-                            { props.isNotLinking && <MainContentBar /> }
+                            {props.isNotLinking && <MainContentBar />}
                         </Grid>
                         <Grid item xs className={props.classes.content}>
                             <Switch>
@@ -235,6 +237,7 @@ export const WorkbenchPanel =
             <ProcessCommandDialog />
             <ProcessInputDialog />
             <ProjectPropertiesDialog />
+            <RestoreCollectionVersionDialog />
             <RemoveApiClientAuthorizationDialog />
             <RemoveComputeNodeDialog />
             <RemoveGroupDialog />
@@ -260,5 +263,6 @@ export const WorkbenchPanel =
             <UserManageDialog />
             <VirtualMachineAttributesDialog />
             <FedLogin />
+            <WebDavS3InfoDialog />
         </Grid>
     );
index b533156de067fb0ac20aec0b63e5bb531b6637e3..d93ec7ae5971cb3e6b529b561f5102690e4daae9 100644 (file)
@@ -8,7 +8,7 @@ Clusters:
       Insecure: true
     Collections:
       CollectionVersioning: true
-      PreserveVersionIfIdle: 0s
+      PreserveVersionIfIdle: -1s
       BlobSigningKey: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
       TrustAllContent: true
       ForwardSlashNameSubstitution: /
index 842d6cf837166f6369177b2ee8b64ae9002c0993..d11e22b6599eb2d312cb4e122ef368c7f38a53a4 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     "@types/react" "*"
     redux "^3.6.0 || ^4.0.0"
 
+"@types/redux-mock-store@1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/redux-mock-store/-/redux-mock-store-1.0.2.tgz#c27d5deadfb29d8514bdb0fc2cadae6feea1922d"
+  integrity sha512-6LBtAQBN34i7SI5X+Qs4zpTEZO1tTDZ6sZ9fzFjYwTl3nLQXaBtwYdoV44CzNnyKu438xJ1lSIYyw0YMvunESw==
+  dependencies:
+    redux "^4.0.5"
+
 "@types/shell-quote@1.6.0":
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.6.0.tgz#537b2949a2ebdcb0d353e448fee45b081021963f"
@@ -6839,6 +6846,11 @@ lodash.isfunction@^3.0.8:
   resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
   integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==
 
+lodash.isplainobject@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+  integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
 lodash.isstring@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
@@ -9317,6 +9329,13 @@ redux-form@7.4.2:
     prop-types "^15.6.1"
     react-lifecycles-compat "^3.0.4"
 
+redux-mock-store@1.5.4:
+  version "1.5.4"
+  resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872"
+  integrity sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==
+  dependencies:
+    lodash.isplainobject "^4.0.6"
+
 redux-thunk@2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
@@ -9348,6 +9367,14 @@ redux@^3.6.0:
     loose-envify "^1.1.0"
     symbol-observable "^1.0.3"
 
+redux@^4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
+  integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
+  dependencies:
+    loose-envify "^1.4.0"
+    symbol-observable "^1.2.0"
+
 reflect.ownkeys@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"