routing+init-data-explorer
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 8 May 2019 11:27:55 +0000 (13:27 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 8 May 2019 11:27:55 +0000 (13:27 +0200)
Feature #15020

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

12 files changed:
src/components/details-attribute/details-attribute.tsx
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/store/collections-content-address-panel/collections-content-address-middleware-service.ts [new file with mode: 0644]
src/store/collections-content-address-panel/collections-content-address-panel-actions.ts [new file with mode: 0644]
src/store/navigation/navigation-action.ts
src/store/store.ts
src/store/workbench/workbench-actions.ts
src/views-components/details-panel/collection-details.tsx
src/views/collection-content-address-panel/collection-content-address-panel.tsx [new file with mode: 0644]
src/views/favorite-panel/favorite-panel.tsx
src/views/workbench/workbench.tsx

index d255d14b1b7538f9bcce620ed705c827d8caef8e..b8955d4e33bedb2fc833ad4ad9adae65be2d516b 100644 (file)
@@ -7,6 +7,7 @@ import Typography from '@material-ui/core/Typography';
 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';
 
@@ -49,23 +50,25 @@ interface DetailsAttributeDataProps {
     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>
 );
index 2811f95aab33525e4e483086989ce3c46280dae4..50ba319ea18315e4e13770cf9f3d36852b5db58f 100644 (file)
@@ -23,7 +23,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     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);
@@ -45,6 +45,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     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());
@@ -102,5 +103,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         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
index 3fd6670d8899814662b413e0898b94191dbe46a1..dd09d8ece55545546c482bf0b5eb5d5398ce51ae 100644 (file)
@@ -34,7 +34,8 @@ export const Routes = {
     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) => {
@@ -136,5 +137,8 @@ export const matchGroupDetailsRoute = (route: 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 });
diff --git a/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts b/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts
new file mode 100644 (file)
index 0000000..53c45d5
--- /dev/null
@@ -0,0 +1,86 @@
+// 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
diff --git a/src/store/collections-content-address-panel/collections-content-address-panel-actions.ts b/src/store/collections-content-address-panel/collections-content-address-panel-actions.ts
new file mode 100644 (file)
index 0000000..11f1a8c
--- /dev/null
@@ -0,0 +1,15 @@
+// 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());
+    };
index af7a0c037357d1104e70c62d84769f044288cfd3..d7ad017878894ff0156333b9d3c290c0637276b3 100644 (file)
@@ -98,3 +98,5 @@ export const navigateToGroups = push(Routes.GROUPS);
 export const navigateToGroupDetails = compose(push, getGroupUrl);
 
 export const navigateToLinks = push(Routes.LINKS);
+
+export const navigateToCollectionsContentAddress = push(Routes.COLLECTIONS_CONTENT_ADDRESS);
index 9d7dcdd4579b41f9295e6dad16bdff1662c2a19d..eff2454a4e46aa40b049df0b6ef1dd4b5cb71bf2 100644 (file)
@@ -62,6 +62,8 @@ import { ApiClientAuthorizationMiddlewareService } from '~/store/api-client-auth
 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' &&
@@ -115,6 +117,10 @@ export function configureStore(history: History, services: ServiceRepository): R
     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),
@@ -130,7 +136,8 @@ export function configureStore(history: History, services: ServiceRepository): R
         linkPanelMiddleware,
         computeNodeMiddleware,
         apiClientAuthorizationMiddlewareService,
-        publicFavoritesMiddleware
+        publicFavoritesMiddleware,
+        collectionsContentAddress
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
index 378bb8de541fedad7af6b9f1b073c6464e2ca94b..25852cbfd91af8185c120251aaa17a5d2f252e72 100644 (file)
@@ -93,6 +93,7 @@ import { groupDetailsPanelColumns } from '~/views/group-details-panel/group-deta
 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';
 
@@ -132,6 +133,7 @@ export const loadWorkbench = () =>
             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) {
@@ -153,6 +155,11 @@ export const loadFavorites = () =>
             dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
         });
 
+export const loadCollectionContentAddress = handleFirstTimeLoad(
+    async (dispatch: Dispatch<any>) => {
+        await dispatch(loadCollectionsContentAddressPanel());
+    });
+
 export const loadTrash = () =>
     handleFirstTimeLoad(
         (dispatch: Dispatch) => {
@@ -255,7 +262,7 @@ export const loadCollection = (uuid: string) =>
                         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 => {
index 01b867b3fc47a7ec0a03cd0e40c990896fbbce05..b47e9edf9c26c31d8953217cdb929fa4859da9cb 100644 (file)
@@ -25,8 +25,8 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
             <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)} />
diff --git a/src/views/collection-content-address-panel/collection-content-address-panel.tsx b/src/views/collection-content-address-panel/collection-content-address-panel.tsx
new file mode 100644 (file)
index 0000000..009e16c
--- /dev/null
@@ -0,0 +1,114 @@
+// 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
index 8498b7876a6d8352bfb760e9629dd4f0b689b1bf..e7234009d9e3baa15442a13ffbbb4bd2d917f55b 100644 (file)
@@ -22,7 +22,6 @@ import {
     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';
index a31c7d25e00939729d2f9d9bf06b7ce04f7caca5..e3668373ff754ce93bb1dbf978ad346461ff894d 100644 (file)
@@ -92,6 +92,7 @@ import { GroupMemberAttributesDialog } from '~/views-components/groups-dialog/me
 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';
 
@@ -176,6 +177,7 @@ export const WorkbenchPanel =
                                 <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>