.yarnrc
.npmrc
src/lib/cwl-svg/*
+tools/arvados_config.yml
// Check that name & uuid are correct.
cy.get('[data-cy=collection-info-panel]')
.should('contain', this.testCollection.name)
- .and('contain', this.testCollection.uuid);
+ .and('contain', this.testCollection.uuid)
+ .and('not.contain', 'This is an old version');
// Check for the read-only icon
cy.get('[data-cy=read-only-icon]').should(`${isWritable ? 'not.' : ''}exist`);
// Check that both read and write operations are available on
})
})
})
+
+ it('can correctly display old versions', function() {
+ const colName = `Versioned Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+ let colUuid = '';
+ let oldVersionUuid = '';
+ // Make sure no other collections with this name exist
+ cy.doRequest('GET', '/arvados/v1/collections', null, {
+ filters: `[["name", "=", "${colName}"]]`,
+ include_old_versions: true
+ })
+ .its('body.items').as('collections')
+ .then(function() {
+ expect(this.collections).to.be.empty;
+ });
+ // Creates the collection using the admin token so we can set up
+ // a bogus manifest text without block signatures.
+ cy.createCollection(adminUser.token, {
+ name: colName,
+ owner_uuid: activeUser.user.uuid,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
+ .as('originalVersion').then(function() {
+ // Change the file name to create a new version.
+ cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n"
+ })
+ colUuid = this.originalVersion.uuid;
+ });
+ // Confirm that there are 2 versions of the collection
+ cy.doRequest('GET', '/arvados/v1/collections', null, {
+ filters: `[["name", "=", "${colName}"]]`,
+ include_old_versions: true
+ })
+ .its('body.items').as('collections')
+ .then(function() {
+ expect(this.collections).to.have.lengthOf(2);
+ this.collections.map(function(aCollection) {
+ expect(aCollection.current_version_uuid).to.equal(colUuid);
+ if (aCollection.uuid !== aCollection.current_version_uuid) {
+ oldVersionUuid = aCollection.uuid;
+ }
+ });
+ // Check the old version displays as what it is.
+ cy.loginAs(activeUser)
+ cy.visit(`/collections/${oldVersionUuid}`);
+ cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
+ cy.get('[data-cy=read-only-icon]').should('exist');
+ cy.get('[data-cy=collection-info-panel]').should('contain', colName);
+ cy.get('[data-cy=collection-files-panel]').should('contain', 'bar');
+ });
+ });
})
//
// SPDX-License-Identifier: AGPL-3.0
-describe('Collection panel tests', function() {
+describe('Favorites tests', function() {
let activeUser;
let adminUser;
})
}, null, activeUser.token, true);
// Should log the user out.
- cy.get('[data-cy=breadcrumb-first]').click();
+ cy.visit('/');
cy.get('div#root').should('contain', 'Please log in');
})
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('Search tests', function() {
+ let activeUser;
+ let adminUser;
+
+ before(function() {
+ // Only set up common users once. These aren't set up as aliases because
+ // aliases are cleaned up after every test. Also it doesn't make sense
+ // to set the same users on beforeEach() over and over again, so we
+ // separate a little from Cypress' 'Best Practices' here.
+ cy.getUser('admin', 'Admin', 'User', true, true)
+ .as('adminUser').then(function() {
+ adminUser = this.adminUser;
+ }
+ );
+ cy.getUser('collectionuser1', 'Collection', 'User', false, true)
+ .as('activeUser').then(function() {
+ activeUser = this.activeUser;
+ }
+ );
+ })
+
+ beforeEach(function() {
+ cy.clearCookies()
+ cy.clearLocalStorage()
+ })
+
+ it('can search for old collection versions', function() {
+ const colName = `Versioned Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+ let colUuid = '';
+ let oldVersionUuid = '';
+ // Make sure no other collections with this name exist
+ cy.doRequest('GET', '/arvados/v1/collections', null, {
+ filters: `[["name", "=", "${colName}"]]`,
+ include_old_versions: true
+ })
+ .its('body.items').as('collections')
+ .then(function() {
+ expect(this.collections).to.be.empty;
+ });
+ // Creates the collection using the admin token so we can set up
+ // a bogus manifest text without block signatures.
+ cy.createCollection(adminUser.token, {
+ name: colName,
+ owner_uuid: activeUser.user.uuid,
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
+ .as('originalVersion').then(function() {
+ // Change the file name to create a new version.
+ cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n"
+ })
+ colUuid = this.originalVersion.uuid;
+ });
+ // Confirm that there are 2 versions of the collection
+ cy.doRequest('GET', '/arvados/v1/collections', null, {
+ filters: `[["name", "=", "${colName}"]]`,
+ include_old_versions: true
+ })
+ .its('body.items').as('collections')
+ .then(function() {
+ expect(this.collections).to.have.lengthOf(2);
+ this.collections.map(function(aCollection) {
+ expect(aCollection.current_version_uuid).to.equal(colUuid);
+ if (aCollection.uuid !== aCollection.current_version_uuid) {
+ oldVersionUuid = aCollection.uuid;
+ }
+ });
+ cy.loginAs(activeUser);
+ const searchQuery = `${colName} type:arvados#collection`;
+ // Search for only collection's current version
+ cy.visit(`/search-results?q=${encodeURIComponent(searchQuery)}`);
+ cy.get('[data-cy=search-results]').should('contain', 'current');
+ cy.get('[data-cy=search-results]').should('not.contain', 'old version');
+ // ...and then, include old versions.
+ cy.visit(`/search-results?q=${encodeURIComponent(searchQuery + ' is:pastVersion')}`);
+ cy.get('[data-cy=search-results]').should('contain', 'current');
+ cy.get('[data-cy=search-results]').should('contain', 'old version');
+ });
+ });
+});
\ No newline at end of file
}
)
+Cypress.Commands.add(
+ "updateCollection", (token, uuid, data) => {
+ return cy.updateResource(token, 'collections', uuid, {
+ collection: JSON.stringify(data)
+ })
+ }
+)
+
Cypress.Commands.add(
"createResource", (token, suffix, data) => {
return cy.doRequest('POST', '/arvados/v1/'+suffix, data, null, token, true)
}
)
+Cypress.Commands.add(
+ "updateResource", (token, suffix, uuid, data) => {
+ return cy.doRequest('PUT', '/arvados/v1/'+suffix+'/'+uuid, data, null, token, true)
+ .its('body').as('resource')
+ .then(function() {
+ return this.resource;
+ })
+ }
+)
+
Cypress.Commands.add(
"loginAs", (user) => {
cy.visit(`/token/?api_token=${user.token}`);
}
}
}
- return "";
+ return "0 B";
};
export const formatTime = (time: number, seconds?: boolean) => {
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
attribute: {
- display: 'flex',
- alignItems: 'flex-start',
- marginBottom: theme.spacing.unit
+ marginBottom: ".6 rem"
},
label: {
boxSizing: 'border-box',
color: theme.palette.grey["500"],
- width: '40%'
+ width: '100%'
},
value: {
boxSizing: 'border-box',
- width: '60%',
alignItems: 'flex-start'
},
lowercaseValue: {
textTransform: 'lowercase'
},
link: {
- width: '60%',
color: theme.palette.primary.main,
textDecoration: 'none',
overflowWrap: 'break-word',
},
copyIcon: {
marginLeft: theme.spacing.unit,
- fontSize: '1.125rem',
color: theme.palette.grey["500"],
- cursor: 'pointer'
+ cursor: 'pointer',
+ display: 'inline',
+ '& svg': {
+ fontSize: '1rem'
+ }
}
});
valueNode = value;
}
return <Typography component="div" className={classes.attribute}>
- <Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
+ <Typography component="div" className={classnames([classes.label, classLabel])}>{label}</Typography>
<Typography
onClick={onValueClick}
- component="span"
+ component="div"
className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
{valueNode}
{children}
{linkToUuid && <Tooltip title="Copy">
- <CopyToClipboard text={linkToUuid || ""} onCopy={() => this.onCopy("Copied")}>
- <CopyIcon className={classes.copyIcon} />
- </CopyToClipboard>
+ <span className={classes.copyIcon}>
+ <CopyToClipboard text={linkToUuid || ""} onCopy={() => this.onCopy("Copied")}>
+ <CopyIcon />
+ </CopyToClipboard>
+ </span>
</Tooltip>}
</Typography>
</Typography>;
import * as React from "react";
import { TreeItem } from "../tree/tree";
-import { ProjectIcon, MoreOptionsIcon, DefaultIcon, CollectionIcon } from "../icon/icon";
+import { DirectoryIcon, MoreOptionsIcon, DefaultIcon, FileIcon } from "../icon/icon";
import { Typography, IconButton, StyleRulesCallback, withStyles, WithStyles, Tooltip } from '@material-ui/core';
import { formatFileSize } from "~/common/formatters";
import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon";
export const getIcon = (type: string) => {
switch (type) {
case 'directory':
- return ProjectIcon;
+ return DirectoryIcon;
case 'file':
- return CollectionIcon;
+ return FileIcon;
default:
return DefaultIcon;
}
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
+import { Badge, Tooltip } from '@material-ui/core';
import Add from '@material-ui/icons/Add';
import ArrowBack from '@material-ui/icons/ArrowBack';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
import GetApp from '@material-ui/icons/GetApp';
import Help from '@material-ui/icons/Help';
import HelpOutline from '@material-ui/icons/HelpOutline';
+import History from '@material-ui/icons/History';
import Inbox from '@material-ui/icons/Inbox';
import Info from '@material-ui/icons/Info';
import Input from '@material-ui/icons/Input';
</div>
</span>;
+export const CollectionOldVersionIcon = (props: any) =>
+ <Tooltip title='Old version'>
+ <Badge badgeContent={<History fontSize='small' />}>
+ <CollectionIcon {...props} />
+ </Badge>
+ </Tooltip>;
+
export type IconType = React.SFC<{ className?: string, style?: object }>;
export const AddIcon: IconType = (props) => <Add {...props} />;
export const CloudUploadIcon: IconType = (props) => <CloudUpload {...props} />;
export const DefaultIcon: IconType = (props) => <RateReview {...props} />;
export const DetailsIcon: IconType = (props) => <Info {...props} />;
+export const DirectoryIcon: IconType = (props) => <Folder {...props} />;
export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
export const EditSavedQueryIcon: IconType = (props) => <Create {...props} />;
export const ExpandIcon: IconType = (props) => <ExpandMoreIcon {...props} />;
export const ErrorIcon: IconType = (props) => <ErrorRoundedIcon style={{color: '#ff0000'}} {...props} />;
export const FavoriteIcon: IconType = (props) => <Star {...props} />;
+export const FileIcon: IconType = (props) => <LibraryBooks {...props} />;
export const HelpIcon: IconType = (props) => <Help {...props} />;
export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
import { collectionFilesItemActionSet, readOnlyCollectionFilesItemActionSet } from '~/views-components/context-menu/action-sets/collection-files-item-action-set';
import { collectionFilesNotSelectedActionSet } from '~/views-components/context-menu/action-sets/collection-files-not-selected-action-set';
import { collectionActionSet, readOnlyCollectionActionSet } from '~/views-components/context-menu/action-sets/collection-action-set';
-import { collectionResourceActionSet } from '~/views-components/context-menu/action-sets/collection-resource-action-set';
import { processActionSet } from '~/views-components/context-menu/action-sets/process-action-set';
import { loadWorkbench } from '~/store/workbench/workbench-actions';
import { Routes } from '~/routes/routes';
addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActionSet);
addMenuActionSet(ContextMenuKind.READONLY_COLLECTION_FILES_ITEM, readOnlyCollectionFilesItemActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
-addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
addMenuActionSet(ContextMenuKind.READONLY_COLLECTION, readOnlyCollectionActionSet);
addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet);
addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
expect(result).toEqual(ContextMenuKind.COLLECTION_ADMIN);
});
- it('should return ContextMenuKind.COLLECTION_RESOURCE', () => {
+ it('should return ContextMenuKind.COLLECTION', () => {
// given
const isAdmin = false;
const isEditable = true;
const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
// then
- expect(result).toEqual(ContextMenuKind.COLLECTION_RESOURCE);
+ expect(result).toEqual(ContextMenuKind.COLLECTION);
});
it('should return ContextMenuKind.READONLY_COLLECTION', () => {
const kind = extractUuidKind(uuid);
switch (kind) {
case ResourceKind.PROJECT:
- return !isAdmin ?
- isEditable ? ContextMenuKind.PROJECT : ContextMenuKind.READONLY_PROJECT :
- ContextMenuKind.PROJECT_ADMIN;
+ return !isAdmin
+ ? isEditable
+ ? ContextMenuKind.PROJECT
+ : ContextMenuKind.READONLY_PROJECT
+ : ContextMenuKind.PROJECT_ADMIN;
case ResourceKind.COLLECTION:
- return !isAdmin ?
- isEditable ? ContextMenuKind.COLLECTION_RESOURCE : ContextMenuKind.READONLY_COLLECTION :
- ContextMenuKind.COLLECTION_ADMIN;
+ return !isAdmin
+ ? isEditable
+ ? ContextMenuKind.COLLECTION
+ : ContextMenuKind.READONLY_COLLECTION
+ : ContextMenuKind.COLLECTION_ADMIN;
case ResourceKind.PROCESS:
- return !isAdmin ? ContextMenuKind.PROCESS_RESOURCE : ContextMenuKind.PROCESS_ADMIN;
+ return !isAdmin
+ ? ContextMenuKind.PROCESS_RESOURCE
+ : ContextMenuKind.PROCESS_ADMIN;
case ResourceKind.USER:
return ContextMenuKind.ROOT_PROJECT;
case ResourceKind.LINK:
if (kind === ResourceKind.COLLECTION) {
dispatch(openInNewTabActions.OPEN_COLLECTION_IN_NEW_TAB(uuid));
- }
- if (kind === ResourceKind.PROJECT) {
+ } else if (kind === ResourceKind.PROJECT) {
dispatch(openInNewTabActions.OPEN_PROJECT_IN_NEW_TAB(uuid));
}
-
- console.log(uuid);
};
\ No newline at end of file
},
{
icon: CopyIcon,
- name: "Copy to project",
+ name: "Make a copy",
execute: (dispatch, resource) => {
dispatch<any>(openCollectionCopyDialog(resource));
}
},
{
icon: CopyIcon,
- name: "Copy to project",
+ name: "Make a copy",
execute: (dispatch, resource) => {
dispatch<any>(openCollectionCopyDialog(resource));
}
dispatch<any>(toggleDetailsPanel());
}
},
- // {
- // icon: ProvenanceGraphIcon,
- // name: "Provenance graph",
- // execute: (dispatch, resource) => {
- // // add code
- // }
- // },
{
icon: AdvancedIcon,
name: "Advanced",
dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
}
},
- // {
- // icon: RemoveIcon,
- // name: "Remove",
- // execute: (dispatch, resource) => {
- // // add code
- // }
- // }
]];
+++ /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 } from '~/components/icon/icon';
-import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
-import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
-import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
-import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
-import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
-import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
-import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
-import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
-
-export const collectionResourceActionSet: ContextMenuActionSet = [[
- {
- icon: RenameIcon,
- name: "Edit collection",
- execute: (dispatch, resource) => {
- dispatch<any>(openCollectionUpdateDialog(resource));
- }
- },
- {
- icon: ShareIcon,
- name: "Share",
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSharingDialog(uuid));
- }
- },
- {
- component: ToggleFavoriteAction,
- execute: (dispatch, resource) => {
- dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
- });
- }
- },
- {
- icon: MoveToIcon,
- name: "Move to",
- execute: (dispatch, resource) => {
- dispatch<any>(openMoveCollectionDialog(resource));
- }
- },
- {
- icon: CopyIcon,
- name: "Copy to project",
- execute: (dispatch, resource) => {
- dispatch<any>(openCollectionCopyDialog(resource));
- }
- },
- {
- icon: DetailsIcon,
- name: "View details",
- execute: dispatch => {
- dispatch<any>(toggleDetailsPanel());
- }
- },
- {
- icon: AdvancedIcon,
- name: "Advanced",
- execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid));
- }
- },
- {
- component: ToggleTrashAction,
- execute: (dispatch, resource) => {
- dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
- }
- },
- // {
- // icon: RemoveIcon,
- // name: "Remove",
- // execute: (dispatch, resource) => {
- // // add code
- // }
- // }
-]];
COLLECTION_FILES_NOT_SELECTED = "CollectionFilesNotSelected",
COLLECTION = 'Collection',
COLLECTION_ADMIN = 'CollectionAdmin',
- COLLECTION_RESOURCE = 'CollectionResource',
READONLY_COLLECTION = 'ReadOnlyCollection',
TRASHED_COLLECTION = 'TrashedCollection',
PROCESS = "Process",
import * as React from 'react';
import { CollectionIcon } from '~/components/icon/icon';
import { CollectionResource } from '~/models/collection';
-import { formatDate, formatFileSize } from '~/common/formatters';
-import { resourceLabel } from '~/common/labels';
-import { ResourceKind } from '~/models/resource';
import { DetailsData } from "./details-data";
-import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
+import { CollectionDetailsAttributes } from '~/views/collection-panel/collection-panel';
export class CollectionDetails extends DetailsData<CollectionResource> {
}
getDetails() {
- return <div>
- <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.COLLECTION)} />
- <DetailsAttribute label='Size' value='---' />
- <DetailsAttribute label='Owner' linkToUuid={this.item.ownerUuid} lowercaseValue={true} />
- <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
- <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
- <DetailsAttribute label='Collection UUID' linkToUuid={this.item.uuid} value={this.item.uuid} />
- <DetailsAttribute label='Content address' linkToUuid={this.item.portableDataHash} value={this.item.portableDataHash} />
- {/* Missing attrs */}
- <DetailsAttribute label='Number of files' value={this.item.fileCount} />
- <DetailsAttribute label='Content size' value={formatFileSize(this.item.fileSizeTotal)} />
- </div>;
+ return <CollectionDetailsAttributes item={this.item} twoCol={false} />;
}
}
<Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
<form onSubmit={this.handleSubmit}>
<Input
+ data-cy='search-input-field'
className={classes.input}
onChange={this.handleChange}
placeholder="Search"
import { RouteComponentProps } from 'react-router';
import { ArvadosTheme } from '~/common/custom-theme';
import { RootState } from '~/store/store';
-import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, ExpandIcon } from '~/components/icon/icon';
+import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, ExpandIcon, CollectionOldVersionIcon } from '~/components/icon/icon';
import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
-import { CollectionResource } from '~/models/collection';
+import { CollectionResource, getCollectionUrl } from '~/models/collection';
import { CollectionPanelFiles } from '~/views-components/collection-panel-files/collection-panel-files';
import { CollectionTagForm } from './collection-tag-form';
import { deleteCollectionTag, navigateToProcess, collectionPanelActions } from '~/store/collection-panel/collection-panel-action';
import { getResource } from '~/store/resources/resources';
import { openContextMenu } from '~/store/context-menu/context-menu-actions';
import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-import { formatFileSize } from "~/common/formatters";
+import { formatDate, formatFileSize } from "~/common/formatters";
import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { getPropertyChip } from '~/views-components/resource-properties-form/property-chip';
import { getUserUuid } from '~/common/getuser';
import { getProgressIndicator } from '~/store/progress-indicator/progress-indicator-reducer';
import { COLLECTION_PANEL_LOAD_FILES, loadCollectionFiles, COLLECTION_PANEL_LOAD_FILES_THRESHOLD } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
+import { Link } from 'react-router-dom';
type CssRules = 'root'
| 'filesCard'
| 'value'
| 'link'
| 'centeredLabel'
+ | 'warningLabel'
+ | 'collectionName'
| 'readOnlyIcon';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
fontSize: '0.875rem',
textAlign: 'center'
},
+ warningLabel: {
+ fontStyle: 'italic'
+ },
+ collectionName: {
+ flexDirection: 'column',
+ },
value: {
textTransform: 'none',
fontSize: '0.875rem'
interface CollectionPanelDataProps {
item: CollectionResource;
isWritable: boolean;
+ isOldVersion: boolean;
isLoadingFiles: boolean;
tooManyFiles: boolean;
}
const currentUserUUID = getUserUuid(state);
const item = getResource<CollectionResource>(props.match.params.id)(state.resources);
let isWritable = false;
- if (item && item.ownerUuid === currentUserUUID) {
- isWritable = true;
- } else if (item) {
- const itemOwner = getResource<GroupResource|UserResource>(item.ownerUuid)(state.resources);
- if (itemOwner) {
- isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0;
+ const isOldVersion = item && item.currentVersionUuid !== item.uuid;
+ if (item && !isOldVersion) {
+ if (item.ownerUuid === currentUserUUID) {
+ isWritable = true;
+ } else {
+ const itemOwner = getResource<GroupResource | UserResource>(item.ownerUuid)(state.resources);
+ if (itemOwner) {
+ isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0;
+ }
}
}
const loadingFilesIndicator = getProgressIndicator(COLLECTION_PANEL_LOAD_FILES)(state.progressIndicator);
const isLoadingFiles = loadingFilesIndicator && loadingFilesIndicator!.working || false;
const tooManyFiles = !state.collectionPanel.loadBigCollections && item && item.fileCount > COLLECTION_PANEL_LOAD_FILES_THRESHOLD || false;
- return { item, isWritable, isLoadingFiles, tooManyFiles };
+ return { item, isWritable, isOldVersion, isLoadingFiles, tooManyFiles };
})(
class extends React.Component<CollectionPanelProps> {
render() {
- const { classes, item, dispatch, isWritable, isLoadingFiles, tooManyFiles } = this.props;
+ const { classes, item, dispatch, isWritable, isOldVersion, isLoadingFiles, tooManyFiles } = this.props;
return item
? <div className={classes.root}>
<ExpansionPanel data-cy='collection-info-panel' defaultExpanded>
<ExpansionPanelSummary expandIcon={<ExpandIcon />}>
- <span>
- <IconButton onClick={this.openCollectionDetails}>
- <CollectionIcon className={classes.iconHeader} />
- </IconButton>
- <IllegalNamingWarning name={item.name}/>
- {item.name}
- {isWritable ||
- <Tooltip title="Read-only">
- <ReadOnlyIcon data-cy="read-only-icon" className={classes.readOnlyIcon} />
- </Tooltip>
- }
- </span>
+ <Grid container justify="space-between">
+ <Grid item xs={11}><span>
+ <IconButton onClick={this.openCollectionDetails}>
+ {isOldVersion
+ ? <CollectionOldVersionIcon className={classes.iconHeader} />
+ : <CollectionIcon className={classes.iconHeader} />}
+ </IconButton>
+ <IllegalNamingWarning name={item.name} />
+ <span>
+ {item.name}
+ {isWritable ||
+ <Tooltip title="Read-only">
+ <ReadOnlyIcon data-cy="read-only-icon" className={classes.readOnlyIcon} />
+ </Tooltip>
+ }
+ </span>
+ </span></Grid>
+ <Grid item xs={1} style={{ textAlign: "right" }}>
+ <Tooltip title="Actions" disableFocusListener>
+ <IconButton
+ data-cy='collection-panel-options-btn'
+ aria-label="Actions"
+ onClick={this.handleContextMenu}>
+ <MoreOptionsIcon />
+ </IconButton>
+ </Tooltip>
+ </Grid>
+ </Grid>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Grid container justify="space-between">
- <Grid item xs={11}>
+ <Grid item xs={12}>
<Typography variant="caption">
{item.description}
</Typography>
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Collection UUID'
- linkToUuid={item.uuid} />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Portable data hash'
- linkToUuid={item.portableDataHash} />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Number of files' value={item.fileCount} />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Content size' value={formatFileSize(item.fileSizeTotal)} />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Owner' linkToUuid={item.ownerUuid} />
+ <CollectionDetailsAttributes item={item} classes={classes} twoCol={true} />
{(item.properties.container_request || item.properties.containerRequest) &&
<span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
<DetailsAttribute classLabel={classes.link} label='Link to process' />
</span>
}
- </Grid>
- <Grid item xs={1} style={{textAlign: "right"}}>
- <Tooltip title="More options" disableFocusListener>
- <IconButton
- data-cy='collection-panel-options-btn'
- aria-label="More options"
- onClick={this.handleContextMenu}>
- <MoreOptionsIcon />
- </IconButton>
- </Tooltip>
+ {isOldVersion &&
+ <Typography className={classes.warningLabel} variant="caption">
+ This is an old version. Make a copy to make changes. Go to the <Link to={getCollectionUrl(item.currentVersionUuid)}>head version</Link> for sharing options.
+ </Typography>
+ }
</Grid>
</Grid>
</ExpansionPanelDetails>
<CollectionTagForm />
</Grid>}
<Grid item xs={12}>
- { Object.keys(item.properties).length > 0
- ? Object.keys(item.properties).map(k =>
- Array.isArray(item.properties[k])
- ? item.properties[k].map((v: string) =>
- getPropertyChip(
- k, v,
- isWritable
- ? this.handleDelete(k, item.properties[k])
- : undefined,
- classes.tag))
- : getPropertyChip(
- k, item.properties[k],
- isWritable
- ? this.handleDelete(k, item.properties[k])
- : undefined,
- classes.tag)
- )
- : <div className={classes.centeredLabel}>No properties set on this collection.</div>
- }
+ {Object.keys(item.properties).length > 0
+ ? Object.keys(item.properties).map(k =>
+ Array.isArray(item.properties[k])
+ ? item.properties[k].map((v: string) =>
+ getPropertyChip(
+ k, v,
+ isWritable
+ ? this.handleDelete(k, item.properties[k])
+ : undefined,
+ classes.tag))
+ : getPropertyChip(
+ k, item.properties[k],
+ isWritable
+ ? this.handleDelete(k, item.properties[k])
+ : undefined,
+ classes.tag)
+ )
+ : <div className={classes.centeredLabel}>No properties set on this collection.</div>
+ }
</Grid>
</Grid>
</ExpansionPanelDetails>
dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true));
dispatch<any>(loadCollectionFiles(this.props.item.uuid));
}
- } />
+ } />
</div>
</div>
: null;
: ContextMenuKind.COLLECTION
: ContextMenuKind.READONLY_COLLECTION
};
+ // Avoid expanding/collapsing the panel
+ event.stopPropagation();
this.props.dispatch<any>(openContextMenu(event, resource));
}
}
)
);
+
+export const CollectionDetailsAttributes = (props: { item: CollectionResource, twoCol: boolean, classes?: Record<CssRules, string> }) => {
+ const item = props.item;
+ const classes = props.classes || { label: '', value: '' };
+ const isOldVersion = item && item.currentVersionUuid !== item.uuid;
+ const mdSize = props.twoCol ? 6 : 12;
+ return <Grid container>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label={isOldVersion ? "This version's UUID" : "Collection UUID"}
+ linkToUuid={item.uuid} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label={isOldVersion ? "This version's PDH" : "Portable data hash"}
+ linkToUuid={item.portableDataHash} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Owner' linkToUuid={item.ownerUuid} />
+ </Grid>
+
+ {isOldVersion &&
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Head version'
+ linkToUuid={item.currentVersionUuid} />
+ </Grid>
+ }
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Version number' value={item.version} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Created at' value={formatDate(item.createdAt)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Last modified' value={formatDate(item.modifiedAt)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Number of files' value={item.fileCount} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Content size' value={formatFileSize(item.fileSizeTotal)} />
+ </Grid>
+ </Grid>;
+};
(props: SearchResultsPanelProps & WithStyles<CssRules, true>) => {
const homeCluster = props.user.uuid.substr(0, 5);
const loggedIn = props.sessions.filter((ss) => ss.loggedIn && ss.userIsActive);
- return <DataExplorer
+ return <span data-cy='search-results'><DataExplorer
id={SEARCH_RESULTS_PANEL_ID}
onRowClick={props.onItemClick}
onRowDoubleClick={props.onItemDoubleClick}
: <span style={{ marginLeft: "2em" }}>Use <Link to={Routes.SITE_MANAGER} >Site Manager</Link> to manage which clusters will be searched.</span>}
</div >
}
- />;
+ /></span>;
});
TLS:
Insecure: true
Collections:
+ CollectionVersioning: true
+ PreserveVersionIfIdle: 0s
BlobSigningKey: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
TrustAllContent: true
ForwardSlashNameSubstitution: /