Merge branch 'master' into 15064-wb2-fed-login
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Wed, 15 May 2019 14:30:38 +0000 (10:30 -0400)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Wed, 15 May 2019 14:30:38 +0000 (10:30 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

25 files changed:
src/components/data-explorer/data-explorer.tsx
src/components/details-attribute/details-attribute.tsx
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/store/collection-panel/collection-panel-action.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/store/collection-panel/collection-panel-reducer.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/owner-name/owner-name-actions.ts [new file with mode: 0644]
src/store/owner-name/owner-name-reducer.ts [new file with mode: 0644]
src/store/projects/project-move-actions.ts
src/store/projects/project-update-actions.ts
src/store/store.ts
src/store/workbench/workbench-actions.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/details-panel/collection-details.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/favorite-star/favorite-star.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/process-log-panel/process-log-main-card.tsx
src/views/search-results-panel/search-results-panel-view.tsx
src/views/workbench/workbench.tsx

index 9f727049b1eda3d7d7f0faf9560c9a1e70d2553b..7107bd70823526226e45aa16687bdbcb5609b38a 100644 (file)
@@ -14,7 +14,7 @@ import { DataTableFilters } from '~/components/data-table-filters/data-table-fil
 import { MoreOptionsIcon } from '~/components/icon/icon';
 import { PaperProps } from '@material-ui/core/Paper';
 
-type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton';
+type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     searchBox: {
@@ -23,6 +23,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
         paddingTop: theme.spacing.unit * 2
     },
+    toolbarUnderTitle: {
+        paddingTop: 0
+    },
     footer: {
         overflow: 'auto'
     },
@@ -31,6 +34,11 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     moreOptionsButton: {
         padding: 0
+    },
+    title: {
+        paddingLeft: theme.spacing.unit * 3,
+        paddingTop: theme.spacing.unit * 3,
+        fontSize: '18px'
     }
 });
 
@@ -50,7 +58,7 @@ interface DataExplorerDataProps<T> {
     paperProps?: PaperProps;
     actions?: React.ReactNode;
     hideSearchInput?: boolean;
-    header?: React.ReactNode;
+    title?: React.ReactNode;
     paperKey?: string;
     currentItemUuid: string;
 }
@@ -85,12 +93,12 @@ export const DataExplorer = withStyles(styles)(
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
-                paperKey, fetchMode, currentItemUuid, header
+                paperKey, fetchMode, currentItemUuid, title
             } = this.props;
             return <Paper className={classes.root} {...paperProps} key={paperKey}>
-                {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={classes.toolbar}>
+                {title && <div className={classes.title}>{title}</div>}
+                {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
-                        {header && <div>{header}</div>}
                         <div className={classes.searchBox}>
                             {!hideSearchInput && <SearchInput
                                 value={searchValue}
index d255d14b1b7538f9bcce620ed705c827d8caef8e..3586d22de99aec68309fd10fadbc3542aa7b4412 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={`/collections/${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 bd47ca2fd38eb752646a03b27365928b7637b22f..831c69e58cc62fcd15b09f135b2de66896cf0089 100644 (file)
@@ -35,7 +35,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) => {
@@ -137,5 +138,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 });
index cd4264710cb8feb93e264a3cfb90c78fd9cc8bce..b1dd8389611611940f2c270889dbaf5663bb3a25 100644 (file)
@@ -7,7 +7,7 @@ import { loadCollectionFiles } from "./collection-panel-files/collection-panel-f
 import { CollectionResource } from '~/models/collection';
 import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
 import { createTree } from "~/models/tree";
-import { RootState } from "../store";
+import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
 import { TagProperty } from "~/models/tag";
 import { snackbarActions } from "../snackbar/snackbar-actions";
@@ -15,6 +15,7 @@ import { resourcesActions } from "~/store/resources/resources-actions";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
 import { SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { navigateTo } from '~/store/navigation/navigation-action';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 export const collectionPanelActions = unionize({
     SET_COLLECTION: ofType<CollectionResource>(),
@@ -31,6 +32,7 @@ export const loadCollectionPanel = (uuid: string) =>
         dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
         const collection = await services.collectionService.get(uuid);
+        dispatch(loadDetailsPanel(collection.uuid));
         dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection }));
         dispatch(resourcesActions.SET_RESOURCES([collection]));
         dispatch<any>(loadCollectionFiles(collection.uuid));
index b75de94a7e890ac74871b496b565bf270c3d861e..534d70d480e2fcf263db814a1e46b539619ababd 100644 (file)
@@ -77,8 +77,8 @@ export const openFileRemoveDialog = (filePath: string) =>
                 ? 'Are you sure you want to remove this directory?'
                 : 'Are you sure you want to remove this file?';
             const info = isDirectory
-                ? 'Removing files will change content adress.'
-                : 'Removing a file will change content adress.';
+                ? 'Removing files will change content address.'
+                : 'Removing a file will change content address.';
 
             dispatch(dialogActions.OPEN_DIALOG({
                 id: FILE_REMOVE_DIALOG,
@@ -101,7 +101,7 @@ export const openMultipleFilesRemoveDialog = () =>
         data: {
             title: 'Removing files',
             text: 'Are you sure you want to remove selected files?',
-            info: 'Removing files will change content adress.',
+            info: 'Removing files will change content address.',
             confirmButtonLabel: 'Remove'
         }
     });
index 55829cb5d094b8e4b9dbddd313dffa256f78eb6f..f09b019873e98e09b082a638a3f12a5b0eea93b2 100644 (file)
@@ -17,5 +17,5 @@ export const collectionPanelReducer = (state: CollectionPanelState = initialStat
     collectionPanelActions.match(action, {
         default: () => state,
         SET_COLLECTION: (item) => ({ ...state, item }),
-        LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item }),
+        LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item })
     });
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..642e7b8
--- /dev/null
@@ -0,0 +1,137 @@
+// 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';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { updateFavorites } from '~/store/favorites/favorites-actions';
+import { updatePublicFavorites } from '~/store/public-favorites/public-favorites-actions';
+import { setBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
+import { ResourceKind, extractUuidKind } from '~/models/resource';
+import { ownerNameActions } from '~/store/owner-name/owner-name-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 userUuid = api.getState().auth.user!.uuid;
+                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)
+                        .addILike("name", dataExplorer.searchValue)
+                        .getFilters()
+                });
+                const userUuids = response.items.map(it => {
+                    if (extractUuidKind(it.ownerUuid) === ResourceKind.USER) {
+                        return it.ownerUuid;
+                    } else {
+                        return '';
+                    }
+                }
+                );
+                const groupUuids = response.items.map(it => {
+                    if (extractUuidKind(it.ownerUuid) === ResourceKind.GROUP) {
+                        return it.ownerUuid;
+                    } else {
+                        return '';
+                    }
+                });
+                const responseUsers = await this.services.userService.list({
+                    limit: dataExplorer.rowsPerPage,
+                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                    filters: new FilterBuilder()
+                        .addIn('uuid', userUuids)
+                        .getFilters()
+                });
+                const responseGroups = await this.services.groupsService.list({
+                    limit: dataExplorer.rowsPerPage,
+                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                    filters: new FilterBuilder()
+                        .addIn('uuid', groupUuids)
+                        .getFilters()
+                });
+                responseUsers.items.map(it=>{
+                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid}));
+                });
+                responseGroups.items.map(it=>{
+                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: `Project: ${it.name}`, uuid: it.uuid}));
+                });
+                api.dispatch<any>(setBreadcrumbs([{ label: 'Projects', uuid: userUuid }]));
+                api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+                api.dispatch<any>(updatePublicFavorites(response.items.map(item => item.uuid)));
+                if (response.itemsAvailable === 1) {
+                    api.dispatch<any>(navigateTo(response.items[0].uuid));
+                    api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+                } else {
+                    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);
diff --git a/src/store/owner-name/owner-name-actions.ts b/src/store/owner-name/owner-name-actions.ts
new file mode 100644 (file)
index 0000000..6c2784a
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+
+export const ownerNameActions = unionize({
+    SET_OWNER_NAME: ofType<OwnerNameState>()
+});
+
+interface OwnerNameState {
+    name: string;
+    uuid: string;
+}
+
+export type OwnerNameAction = UnionOf<typeof ownerNameActions>;
diff --git a/src/store/owner-name/owner-name-reducer.ts b/src/store/owner-name/owner-name-reducer.ts
new file mode 100644 (file)
index 0000000..58df209
--- /dev/null
@@ -0,0 +1,11 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ownerNameActions, OwnerNameAction } from './owner-name-actions';
+
+export const ownerNameReducer = (state = [], action: OwnerNameAction) =>
+    ownerNameActions.match(action, {
+        SET_OWNER_NAME: data => [...state, { uuid: data.uuid, name: data.name }],
+        default: () => state,
+    });
\ No newline at end of file
index 365e07aab61cd662e427d5bb6e05c1998ca693ee..8876be0f252afedd7e1dd0b752a6b4eba8981cf8 100644 (file)
@@ -11,6 +11,8 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from "~/ser
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
 import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { loadSidePanelTreeProjects } from '../side-panel-tree/side-panel-tree-actions';
 
 export const PROJECT_MOVE_FORM_NAME = 'projectMoveFormName';
 
@@ -24,11 +26,14 @@ export const openMoveProjectDialog = (resource: { name: string, uuid: string })
 
 export const moveProject = (resource: MoveToFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const userUuid = getState().auth.user!.uuid;
         dispatch(startSubmit(PROJECT_MOVE_FORM_NAME));
         try {
             const project = await services.projectService.get(resource.uuid);
             const newProject = await services.projectService.update(resource.uuid, { ...project, ownerUuid: resource.ownerUuid });
+            dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
+            await dispatch<any>(loadSidePanelTreeProjects(userUuid));
             return newProject;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
index 321b85548610298005299b3f64b5911d11437113..b92069762a0e300e62044f0f289510fae68911ae 100644 (file)
@@ -11,6 +11,7 @@ import { ServiceRepository } from "~/services/services";
 import { ProjectResource } from '~/models/project';
 import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
 import { getResource } from '~/store/resources/resources';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
@@ -33,6 +34,7 @@ export const updateProject = (project: Partial<ProjectResource>) =>
         dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME));
         try {
             const updatedProject = await services.projectService.update(uuid, project);
+            dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
             return updatedProject;
         } catch (e) {
index 9d7dcdd4579b41f9295e6dad16bdff1662c2a19d..ff9a495e804b9e30c2f6693008d42e5a002943e5 100644 (file)
@@ -62,6 +62,9 @@ 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';
+import { ownerNameReducer } from '~/store/owner-name/owner-name-reducer';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -115,6 +118,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 +137,8 @@ export function configureStore(history: History, services: ServiceRepository): R
         linkPanelMiddleware,
         computeNodeMiddleware,
         apiClientAuthorizationMiddlewareService,
-        publicFavoritesMiddleware
+        publicFavoritesMiddleware,
+        collectionsContentAddress
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
@@ -146,6 +154,7 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     detailsPanel: detailsPanelReducer,
     dialog: dialogReducer,
     favorites: favoritesReducer,
+    ownerName: ownerNameReducer,
     publicFavorites: publicFavoritesReducer,
     form: formReducer,
     processLogsPanel: processLogsPanelReducer,
index adf3fa15aeeb416f927ed99a3f483e4ae7894325..2363b5795a8b9c2c0d0743524fb8e82f460f0caf 100644 (file)
@@ -68,8 +68,7 @@ import { FilterBuilder } from '~/services/api/filter-builder';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
 import { MatchCases, ofType, unionize, UnionOf } from '~/common/unionize';
 import { loadRunProcessPanel } from '~/store/run-process-panel/run-process-panel-actions';
-import { loadCollectionFiles } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
-import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
+import { collectionPanelActions, loadCollectionPanel } from "~/store/collection-panel/collection-panel-action";
 import { CollectionResource } from "~/models/collection";
 import {
     loadSearchResultsPanel,
@@ -94,6 +93,8 @@ 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';
+import { collectionContentAddressPanelColumns } from '~/views/collection-content-address-panel/collection-content-address-panel';
 
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
 
@@ -133,6 +134,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: collectionContentAddressPanelColumns }));
 
             dispatch<any>(initSidePanelTree());
             if (router.location) {
@@ -154,6 +156,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) => {
@@ -257,21 +264,21 @@ export const loadCollection = (uuid: string) =>
                         dispatch(updateResources([collection]));
                         await dispatch(activateSidePanelTreeItem(collection.ownerUuid));
                         dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
-                        dispatch(loadCollectionFiles(collection.uuid));
+                        dispatch(loadCollectionPanel(collection.uuid));
                     },
                     SHARED: collection => {
                         dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
                         dispatch(updateResources([collection]));
                         dispatch<any>(setSharedWithMeBreadcrumbs(collection.ownerUuid));
                         dispatch(activateSidePanelTreeItem(collection.ownerUuid));
-                        dispatch(loadCollectionFiles(collection.uuid));
+                        dispatch(loadCollectionPanel(collection.uuid));
                     },
                     TRASHED: collection => {
                         dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
                         dispatch(updateResources([collection]));
                         dispatch(setTrashBreadcrumbs(''));
                         dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
-                        dispatch(loadCollectionFiles(collection.uuid));
+                        dispatch(loadCollectionPanel(collection.uuid));
                     },
 
                 });
index 5dc1e02aea789f72c55bdb3bd4c1793ec32f1bdd..bf504f2b46b82e4efa475c1359cdf1a7c2c55f9e 100644 (file)
@@ -27,13 +27,13 @@ import { LinkResource } from '~/models/link';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { withResourceData } from '~/views-components/data-explorer/with-resources';
 
-const renderName = (item: { name: string; uuid: string, kind: string }) =>
+const renderName = (dispatch: Dispatch, item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
             {renderIcon(item.kind)}
         </Grid>
         <Grid item>
-            <Typography color="primary" style={{ width: 'auto' }}>
+            <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
                 {item.name}
             </Typography>
         </Grid>
@@ -49,7 +49,7 @@ export const ResourceName = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return resource || { name: '', uuid: '', kind: '' };
-    })(renderName);
+    })((resource: { name: string; uuid: string, kind: string } & DispatchProp<any>) => renderName(resource.dispatch, resource));
 
 const renderIcon = (kind: string) => {
     switch (kind) {
@@ -401,7 +401,7 @@ export const ResourceFileSize = connect(
     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
 
 const renderOwner = (owner: string) =>
-    <Typography noWrap color="primary" >
+    <Typography noWrap>
         {owner}
     </Typography>;
 
@@ -411,6 +411,14 @@ export const ResourceOwner = connect(
         return { owner: resource ? resource.ownerUuid : '' };
     })((props: { owner: string }) => renderOwner(props.owner));
 
+export const ResourceOwnerName = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
+        const ownerNameState = state.ownerName;
+        const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
+        return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
+    })((props: { owner: string }) => renderOwner(props.owner));
+
 const renderType = (type: string) =>
     <Typography noWrap>
         {resourceLabel(type)}
index 98fa388640757d4bd5a09d9cfbdd9d806a617b47..ec5bdabd833a097cac06b3eda5c30805ac5193be 100644 (file)
@@ -24,9 +24,8 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
             <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
             <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.item.portableDataHash} value={this.item.portableDataHash} />
+            <DetailsAttribute label='Collection UUID' linkInsideCard={this.item.uuid} value={this.item.uuid} />
+            <DetailsAttribute label='Content address' linkInsideCard={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)} />
index 2a30ae4783d75d426d29bc82574cd19a086058b3..9ce84867d736d5061d9461be923346c9dc3da604 100644 (file)
@@ -85,7 +85,7 @@ const mapStateToProps = ({ detailsPanel, resources, resourcesData, collectionPan
     const resourceData = getResourceData(detailsPanel.resourceUuid)(resourcesData);
     return {
         isOpened: detailsPanel.isOpened,
-        item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData)
+        item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData),
     };
 };
 
index 6812535def0c8092fdb222e2fae899aac5b527a8..0598e5f5226e104555f3d143335015f47b2514d3 100644 (file)
@@ -6,7 +6,7 @@ import * as React from "react";
 import { FavoriteIcon, PublicFavoriteIcon } from "~/components/icon/icon";
 import { connect } from "react-redux";
 import { RootState } from "~/store/store";
-import { withStyles, StyleRulesCallback, WithStyles } from "@material-ui/core";
+import { withStyles, StyleRulesCallback, WithStyles, Tooltip } from "@material-ui/core";
 
 type CssRules = "icon";
 
@@ -25,7 +25,7 @@ const mapStateToProps = (state: RootState, props: { resourceUuid: string; classN
 export const FavoriteStar = connect(mapStateToProps)(
     withStyles(styles)((props: { isFavoriteVisible: boolean; className?: string; } & WithStyles<CssRules>) => {
         if (props.isFavoriteVisible) {
-            return <FavoriteIcon className={props.className || props.classes.icon} />;
+            return <Tooltip enterDelay={500} title="Favorite"><FavoriteIcon className={props.className || props.classes.icon} /></Tooltip>;
         } else {
             return null;
         }
@@ -34,7 +34,7 @@ export const FavoriteStar = connect(mapStateToProps)(
 export const PublicFavoriteStar = connect(mapStateToProps)(
     withStyles(styles)((props: { isPublicFavoriteVisible: boolean; className?: string; } & WithStyles<CssRules>) => {
         if (props.isPublicFavoriteVisible) {
-            return <PublicFavoriteIcon className={props.className || props.classes.icon} />;
+            return <Tooltip enterDelay={500} title="Public Favorite"><PublicFavoriteIcon className={props.className || props.classes.icon} /></Tooltip>;
         } else {
             return null;
         }
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..b652b50
--- /dev/null
@@ -0,0 +1,154 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+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 { 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 } from 'react-redux';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { DataColumns } from '~/components/data-table/data-table';
+import { SortDirection } from '~/components/data-table/data-column';
+import { createTree } from '~/models/tree';
+import { ResourceName, ResourceOwnerName, ResourceLastModifiedDate } from '~/views-components/data-explorer/renderers';
+
+type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    backLink: {
+        fontSize: '14px',
+        fontWeight: 600,
+        display: 'flex',
+        alignItems: 'center',
+        padding: theme.spacing.unit,
+        marginBottom: 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'
+        }
+    }
+});
+
+enum CollectionContentAddressPanelColumnNames {
+    COLLECTION_WITH_THIS_ADDRESS = "Collection with this address",
+    LOCATION = "Location",
+    LAST_MODIFIED = "Last modified"
+}
+
+export const collectionContentAddressPanelColumns: DataColumns<string> = [
+    {
+        name: CollectionContentAddressPanelColumnNames.COLLECTION_WITH_THIS_ADDRESS,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceName uuid={uuid} />
+    },
+    {
+        name: CollectionContentAddressPanelColumnNames.LOCATION,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceOwnerName uuid={uuid} />
+    },
+    {
+        name: CollectionContentAddressPanelColumnNames.LAST_MODIFIED,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.DESC,
+        filters: createTree(),
+        render: uuid => <ResourceLastModifiedDate uuid={uuid} />
+    }
+];
+
+export interface CollectionContentAddressPanelActionProps {
+    onContextMenu: (event: React.MouseEvent<any>, uuid: string) => void;
+    onItemClick: (item: string) => void;
+    onItemDoubleClick: (item: string) => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): CollectionContentAddressPanelActionProps => ({
+    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));
+    }
+});
+
+interface CollectionContentAddressDataProps {
+    match: {
+        params: { id: string }
+    };
+}
+
+export const CollectionsContentAddressPanel = withStyles(styles)(
+    connect(null, mapDispatchToProps)(
+        class extends React.Component<CollectionContentAddressPanelActionProps & CollectionContentAddressDataProps & WithStyles<CssRules>> {
+            render() {
+                return <Grid item xs={12}>
+                    <Button
+                        onClick={() => history.back()}
+                        className={this.props.classes.backLink}>
+                        <BackIcon className={this.props.classes.backIcon} />
+                        Back
+                    </Button>
+                    <DataExplorer
+                        id={COLLECTIONS_CONTENT_ADDRESS_PANEL_ID}
+                        onRowClick={this.props.onItemClick}
+                        onRowDoubleClick={this.props.onItemDoubleClick}
+                        onContextMenu={this.props.onContextMenu}
+                        contextMenuColumn={true}
+                        title={`Content address: ${this.props.match.params.id}`}
+                        dataTableDefaultView={
+                            <DataTableDefaultView
+                                icon={CollectionIcon}
+                                messages={['Collections with this content address not found.']} />
+                        } />;
+                    </Grid >;
+            }
+        }
+    )
+);
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 6b2521c083e70dc01e9372f884189b1cce3cd59d..ec6a912a71f8b6dae38296539458020a651ed5cf 100644 (file)
@@ -21,7 +21,7 @@ type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'lin
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     backLink: {
-        fontSize: '1rem',
+        fontSize: '14px',
         fontWeight: 600,
         display: 'flex',
         alignItems: 'center',
@@ -72,7 +72,7 @@ export const ProcessLogMainCard = withStyles(styles)(
     ({ classes, process, selectedFilter, filters, onChange, lines, onContextMenu, navigateToLogCollection }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
         <Grid item xs={12}>
             <Link to={`/processes/${process.containerRequest.uuid}`} className={classes.backLink}>
-                <BackIcon className={classes.backIcon} /> Back
+                <BackIcon className={classes.backIcon} /> BACK
             </Link>
             <Card className={classes.card}>
                 <CardHeader
index df6a7e8c1ef0b2957715e2c40c4ab53203c2714c..72a9b282824888e1142c5b85edea3b586ef36890 100644 (file)
@@ -127,10 +127,10 @@ export const SearchResultsPanelView = (props: SearchResultsPanelProps) => {
         onContextMenu={props.onContextMenu}
         contextMenuColumn={true}
         hideSearchInput
-        header={
+        title={
             props.localCluster === homeCluster ?
-                <p>Searching clusters: {props.sessions.filter((ss) => ss.loggedIn).map((ss) => <span key={ss.clusterId}> {ss.clusterId}</span>)}</p> :
-                <p>Searching local cluster {props.localCluster} only.  To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></p>
+                <div>Searching clusters: {props.sessions.filter((ss) => ss.loggedIn).map((ss) => <span key={ss.clusterId}> {ss.clusterId}</span>)}</div> :
+                <div>Searching local cluster {props.localCluster} only.  To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></div>
         }
     />;
 };
index e0997ea16eb1b78725266a039049021cac489860..20cbbdea0c9a0b262f976889d5e097243b777c6f 100644 (file)
@@ -93,6 +93,7 @@ import { AddGroupMembersDialog } from '~/views-components/dialog-forms/add-group
 import { PartialCopyToCollectionDialog } from '~/views-components/dialog-forms/partial-copy-to-collection-dialog';
 import { PublicFavoritePanel } from '~/views/public-favorites-panel/public-favorites-panel';
 import { FedLogin } from './fed-login';
+import { CollectionsContentAddressPanel } from '~/views/collection-content-address-panel/collection-content-address-panel';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -177,6 +178,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>