})
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)
['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]')
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]')
['//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]')
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.
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
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]')
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();
// (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');
});
});
})
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.
"@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"
children?: React.ReactNode;
onValueClick?: () => void;
linkToUuid?: string;
+ copyValue?: string;
}
type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules> & FederationConfig & DispatchProp;
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) {
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>
--- /dev/null
+// 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"/>');
+ });
+});
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;
<img
className={classes.thumbnail}
alt={file.name}
- src={file.url} />
+ src={sanitizeToken(file.url)} />
);
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';
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"
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} />;
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} />;
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} />;
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';
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";
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);
]);
}
+ 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) {
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) => {
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
}));
--- /dev/null
+// 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
+ }
+ }));
+ };
--- /dev/null
+// 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
+ }));
+ }
+ };
//
// 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}.`);
+ }
});
});
});
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';
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 }>(),
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,
kind: res.kind,
menuKind,
ownerUuid: res.ownerUuid,
- isTrashed: res.isTrashed
+ isTrashed: ('isTrashed' in res) ? res.isTrashed: false,
}));
}
};
}
};
-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;
+ }
+ };
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) {
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}',
},
},
});
},
runtimeConstraints: {
API: true,
- ram: 1073741824,
+ ram: 16256 * (1024 * 1024),
vcpus: 1,
},
schedulingParameters: { max_run_time: undefined },
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; }
}
}
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]
export const DEFAULT_ADVANCED_FORM_VALUES: Partial<RunProcessAdvancedFormData> = {
[VCPUS_FIELD]: 1,
[RAM_FIELD]: 1073741824,
+ [KEEP_CACHE_RAM_FIELD]: 268435456,
[RUNNER_IMAGE_FIELD]: "arvados/jobs"
};
--- /dev/null
+// 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
//
// 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",
},
]];
+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), []),
},
]
];
+
+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));
+ }
+ },
+ ]
+];
+++ /dev/null
-// 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!!));
- }
- },
-]];
+++ /dev/null
-// 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
- // }
- // }
-]];
// 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";
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 = [[
{
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 = [
// 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',
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!!));
- }
- },
+ }
]];
COLLECTION = 'Collection',
COLLECTION_ADMIN = 'CollectionAdmin',
READONLY_COLLECTION = 'ReadOnlyCollection',
+ OLD_VERSION_COLLECTION = 'OldVersionCollection',
TRASHED_COLLECTION = 'TrashedCollection',
PROCESS = "Process",
PROCESS_ADMIN = 'ProcessAdmin',
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';
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,
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}>
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}>
--- /dev/null
+// 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");
+ });
+
+});
--- /dev/null
+// 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 → 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 → 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 → 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 >;
+ }
+);
// 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';
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';
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';
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: '',
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';
}
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();
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 {
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";
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";
interface FavoritePanelDataProps {
favorites: FavoritesState;
resources: ResourcesState;
- isAdmin: boolean;
userUuid: string;
}
const mapStateToProps = (state : RootState): FavoritePanelDataProps => ({
favorites: state.favorites,
resources: state.resources,
- isAdmin: state.auth.user!.isAdmin,
userUuid: state.auth.user!.uuid,
});
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: '',
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 => {
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: '',
uuidPrefix={uuidPrefix}
siteBanner={siteBanner}>
{working
- ? <LinearProgress color="secondary" />
+ ? <LinearProgress color="secondary" data-cy="linear-progress" />
: null}
</MainAppBar>}
<Grid container direction="column" className={classes.root}>
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,
} 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";
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> {
}
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
}));
} 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";
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";
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: '',
[RUNTIME_FIELD]?: number;
[RAM_FIELD]: number;
[VCPUS_FIELD]: number;
- [KEEP_CACHE_RAM_FIELD]?: number;
+ [KEEP_CACHE_RAM_FIELD]: number;
[RUNNER_IMAGE_FIELD]: string;
}
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";
interface SharedWithMePanelDataProps {
resources: ResourcesState;
- isAdmin: boolean;
userUuid: string;
}
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> {
}
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
}));
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
};
};
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';
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>
<ProcessCommandDialog />
<ProcessInputDialog />
<ProjectPropertiesDialog />
+ <RestoreCollectionVersionDialog />
<RemoveApiClientAuthorizationDialog />
<RemoveComputeNodeDialog />
<RemoveGroupDialog />
<UserManageDialog />
<VirtualMachineAttributesDialog />
<FedLogin />
+ <WebDavS3InfoDialog />
</Grid>
);
Insecure: true
Collections:
CollectionVersioning: true
- PreserveVersionIfIdle: 0s
+ PreserveVersionIfIdle: -1s
BlobSigningKey: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
TrustAllContent: true
ForwardSlashNameSubstitution: /
"@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"
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"
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"
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"