import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { ArvadosTheme } from '~/common/custom-theme';
import * as classnames from "classnames";
+import { Link } from 'react-router-dom';
type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link';
link?: string;
children?: React.ReactNode;
onValueClick?: () => void;
+ linkInsideCard?: string;
}
type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
export const DetailsAttribute = withStyles(styles)(
- ({ label, link, value, children, classes, classLabel, classValue, lowercaseValue, onValueClick }: DetailsAttributeProps) =>
+ ({ label, link, value, children, classes, classLabel, classValue, lowercaseValue, onValueClick, linkInsideCard }: DetailsAttributeProps) =>
<Typography component="div" className={classes.attribute}>
<Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
- { link
- ? <a href={link} className={classes.link} target='_blank'>{value}</a>
- : <Typography
- onClick={onValueClick}
- component="span"
- className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}
- >
- {value}
- {children}
- </Typography> }
+ {link && <a href={link} className={classes.link} target='_blank'>{value}</a>}
+ {linkInsideCard && <Link to={linkInsideCard} className={classes.link}>{value}</Link>}
+ {!link && !linkInsideCard && <Typography
+ onClick={onValueClick}
+ component="span"
+ className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
+ {value}
+ {children}
+ </Typography>
+ }
+
</Typography>
);
const projectMatch = Routes.matchProjectRoute(pathname);
const collectionMatch = Routes.matchCollectionRoute(pathname);
const favoriteMatch = Routes.matchFavoritesRoute(pathname);
- const publicFavoritesMatch = Routes.matchPublicFavorites(pathname);
+ const publicFavoritesMatch = Routes.matchPublicFavoritesRoute(pathname);
const trashMatch = Routes.matchTrashRoute(pathname);
const processMatch = Routes.matchProcessRoute(pathname);
const processLogMatch = Routes.matchProcessLogRoute(pathname);
const groupsMatch = Routes.matchGroupsRoute(pathname);
const groupDetailsMatch = Routes.matchGroupDetailsRoute(pathname);
const linksMatch = Routes.matchLinksRoute(pathname);
+ const collectionsContentAddressMatch = Routes.matchCollectionsContentAddressRoute(pathname);
store.dispatch(dialogActions.CLOSE_ALL_DIALOGS());
store.dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
store.dispatch(WorkbenchActions.loadGroupDetailsPanel(groupDetailsMatch.params.id));
} else if (linksMatch) {
store.dispatch(WorkbenchActions.loadLinks);
+ } else if (collectionsContentAddressMatch) {
+ store.dispatch(WorkbenchActions.loadCollectionContentAddress);
}
};
\ No newline at end of file
GROUPS: '/groups',
GROUP_DETAILS: `/group/:id(${RESOURCE_UUID_PATTERN})`,
LINKS: '/links',
- PUBLIC_FAVORITES: '/public-favorites'
+ PUBLIC_FAVORITES: '/public-favorites',
+ COLLECTIONS_CONTENT_ADDRESS: '/collections/:id',
};
export const getResourceUrl = (uuid: string) => {
export const matchLinksRoute = (route: string) =>
matchPath(route, { path: Routes.LINKS });
-export const matchPublicFavorites = (route: string) =>
+export const matchPublicFavoritesRoute = (route: string) =>
matchPath(route, { path: Routes.PUBLIC_FAVORITES });
+
+export const matchCollectionsContentAddressRoute = (route: string) =>
+ matchPath(route, { path: Routes.COLLECTIONS_CONTENT_ADDRESS });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { resourcesActions } from '~/store/resources/resources-actions';
+import { FilterBuilder } from '~/services/api/filter-builder';
+import { SortDirection } from '~/components/data-table/data-column';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { FavoritePanelColumnNames } from '~/views/favorite-panel/favorite-panel';
+import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
+import { collectionsContentAddressActions } from './collections-content-address-panel-actions';
+
+export class CollectionsWithSameContentAddressMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
+ if (!dataExplorer) {
+ api.dispatch(collectionPanelDataExplorerIsNotSet());
+ } else {
+ const sortColumn = getSortColumn(dataExplorer);
+
+ const contentOrder = new OrderBuilder<GroupContentsResource>();
+
+ if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
+ const direction = sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+
+ contentOrder
+ .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION);
+ }
+ try {
+ api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+ const pathname = api.getState().router.location!.pathname;
+ const contentAddress = pathname.split('/')[2];
+ const response = await this.services.collectionService.list({
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ filters: new FilterBuilder()
+ .addEqual('portableDataHash', contentAddress)
+ .getFilters()
+ });
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ api.dispatch(resourcesActions.SET_RESOURCES(response.items));
+ api.dispatch(collectionsContentAddressActions.SET_ITEMS({
+ items: response.items.map((resource: any) => resource.uuid),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ } catch (e) {
+ api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ api.dispatch(collectionsContentAddressActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ api.dispatch(couldNotFetchCollections());
+ }
+ }
+ }
+}
+
+const collectionPanelDataExplorerIsNotSet = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Collection panel is not ready.',
+ kind: SnackbarKind.ERROR
+ });
+
+const couldNotFetchCollections = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch collection with this content address.',
+ kind: SnackbarKind.ERROR
+ });
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+
+export const COLLECTIONS_CONTENT_ADDRESS_PANEL_ID = 'collectionsContentAddressPanel';
+
+export const collectionsContentAddressActions = bindDataExplorerActions(COLLECTIONS_CONTENT_ADDRESS_PANEL_ID);
+
+export const loadCollectionsContentAddressPanel = () =>
+ (dispatch: Dispatch) => {
+ dispatch(collectionsContentAddressActions.REQUEST_ITEMS());
+ };
export const navigateToGroupDetails = compose(push, getGroupUrl);
export const navigateToLinks = push(Routes.LINKS);
+
+export const navigateToCollectionsContentAddress = push(Routes.COLLECTIONS_CONTENT_ADDRESS);
import { PublicFavoritesMiddlewareService } from '~/store/public-favorites-panel/public-favorites-middleware-service';
import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
import { publicFavoritesReducer } from '~/store/public-favorites/public-favorites-reducer';
+import { CollectionsWithSameContentAddressMiddlewareService } from '~/store/collections-content-address-panel/collections-content-address-middleware-service';
+import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
const publicFavoritesMiddleware = dataExplorerMiddleware(
new PublicFavoritesMiddlewareService(services, PUBLIC_FAVORITE_PANEL_ID)
);
+ const collectionsContentAddress = dataExplorerMiddleware(
+ new CollectionsWithSameContentAddressMiddlewareService(services, COLLECTIONS_CONTENT_ADDRESS_PANEL_ID)
+ );
+
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
linkPanelMiddleware,
computeNodeMiddleware,
apiClientAuthorizationMiddlewareService,
- publicFavoritesMiddleware
+ publicFavoritesMiddleware,
+ collectionsContentAddress
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
import { DataTableFetchMode } from "~/components/data-table/data-table";
import { loadPublicFavoritePanel, publicFavoritePanelActions } from '~/store/public-favorites-panel/public-favorites-action';
import { publicFavoritePanelColumns } from '~/views/public-favorites-panel/public-favorites-panel';
+import { loadCollectionsContentAddressPanel, collectionsContentAddressActions } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
+ dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: projectPanelColumns }));
dispatch<any>(initSidePanelTree());
if (router.location) {
dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
});
+export const loadCollectionContentAddress = handleFirstTimeLoad(
+ async (dispatch: Dispatch<any>) => {
+ await dispatch(loadCollectionsContentAddressPanel());
+ });
+
export const loadTrash = () =>
handleFirstTimeLoad(
(dispatch: Dispatch) => {
dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
dispatch(updateResources([collection]));
await dispatch(activateSidePanelTreeItem(collection.ownerUuid));
- dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
+ dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
dispatch(loadCollectionPanel(collection.uuid));
},
SHARED: collection => {
<DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
<DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
{/* Links but we dont have view */}
- <DetailsAttribute label='Collection UUID' link={this.item.uuid} value={this.item.uuid} />
- <DetailsAttribute label='Content address' link={this.numberOfCollectionsByPDH === 1 ? this.item.uuid : this.item.portableDataHash} value={this.item.portableDataHash} />
+ <DetailsAttribute label='Collection UUID' linkInsideCard={this.item.uuid} value={this.item.uuid} />
+ <DetailsAttribute label='Content address' linkInsideCard={this.numberOfCollectionsByPDH === 1 ? this.item.uuid : this.item.portableDataHash} value={this.item.portableDataHash} />
{/* Missing attrs */}
<DetailsAttribute label='Number of files' value={this.data && this.data.fileCount} />
<DetailsAttribute label='Content size' value={formatFileSize(this.data && this.data.fileSize)} />
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Link } from 'react-router-dom';
+import {
+ StyleRulesCallback, WithStyles, withStyles, Grid
+} from '@material-ui/core';
+import { CollectionIcon } from '~/components/icon/icon';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { BackIcon } from '~/components/icon/icon';
+import { CollectionResource } from '~/models/collection';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+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 { ResourceKind } from '~/models/resource';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { connect, DispatchProp } from 'react-redux';
+import { navigateTo } from '~/store/navigation/navigation-action';
+
+type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ backLink: {
+ fontSize: '1rem',
+ fontWeight: 600,
+ display: 'flex',
+ alignItems: 'center',
+ textDecoration: 'none',
+ padding: theme.spacing.unit,
+ color: theme.palette.grey["700"],
+ },
+ backIcon: {
+ marginRight: theme.spacing.unit
+ },
+ card: {
+ width: '100%'
+ },
+ title: {
+ color: theme.palette.grey["700"]
+ },
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.green700
+ },
+ link: {
+ fontSize: '0.875rem',
+ color: theme.palette.primary.main,
+ textAlign: 'right',
+ '&:hover': {
+ cursor: 'pointer'
+ }
+ }
+});
+
+
+export interface CollectionContentAddressMainCardActionProps {
+ onContextMenu: (event: React.MouseEvent<any>, uuid: string) => void;
+ onItemClick: (item: string) => void;
+ onItemDoubleClick: (item: string) => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): CollectionContentAddressMainCardActionProps => ({
+ onContextMenu: (event, resourceUuid) => {
+ const isAdmin = dispatch<any>(getIsAdmin());
+ const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin);
+ if (kind) {
+ dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: resourceUuid,
+ ownerUuid: '',
+ kind: ResourceKind.NONE,
+ menuKind: kind
+ }));
+ }
+ dispatch<any>(loadDetailsPanel(resourceUuid));
+ },
+ onItemClick: (uuid: string) => {
+ dispatch<any>(loadDetailsPanel(uuid));
+ },
+ onItemDoubleClick: uuid => {
+ dispatch<any>(navigateTo(uuid));
+ }
+});
+
+export const CollectionsContentAddressPanel = withStyles(styles)(
+ connect(null, mapDispatchToProps)(
+ class extends React.Component<CollectionContentAddressMainCardActionProps & WithStyles<CssRules>> {
+ render() {
+ return <Grid item xs={12}>
+ {/* <Link to={`/collections/${this.props.collection.uuid}`} className={this.props.classes.backLink}>
+ <BackIcon className={this.props.classes.backIcon} />
+ Back test
+ </Link> */}
+ <DataExplorer
+ id={COLLECTIONS_CONTENT_ADDRESS_PANEL_ID}
+ onRowClick={this.props.onItemClick}
+ onRowDoubleClick={this.props.onItemDoubleClick}
+ onContextMenu={this.props.onContextMenu}
+ contextMenuColumn={true}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={CollectionIcon}
+ messages={['Collections with this content address not found.']} />
+ } />;
+ </Grid >;
+ }
+ }
+ )
+);
\ No newline at end of file
ResourceType
} from '~/views-components/data-explorer/renderers';
import { FavoriteIcon } from '~/components/icon/icon';
-import { Dispatch } from 'redux';
import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
import { navigateTo } from '~/store/navigation/navigation-action';
import { AddGroupMembersDialog } from '~/views-components/dialog-forms/add-group-member-dialog';
import { PartialCopyToCollectionDialog } from '~/views-components/dialog-forms/partial-copy-to-collection-dialog';
import { PublicFavoritePanel } from '~/views/public-favorites-panel/public-favorites-panel';
+import { CollectionsContentAddressPanel } from '~/views/collection-content-address-panel/collection-content-address-panel';
type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
<Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
<Route path={Routes.LINKS} component={LinkPanel} />
<Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
+ <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel}/>
</Switch>
</Grid>
</Grid>