From 2d9dcf3d61d410328e081b5b00c7175c7eb1d82b Mon Sep 17 00:00:00 2001 From: Pawel Kowalczyk Date: Wed, 8 May 2019 13:27:55 +0200 Subject: [PATCH] routing+init-data-explorer Feature #15020 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- .../details-attribute/details-attribute.tsx | 25 ++-- src/routes/route-change-handlers.ts | 5 +- src/routes/routes.ts | 8 +- ...ions-content-address-middleware-service.ts | 86 +++++++++++++ ...llections-content-address-panel-actions.ts | 15 +++ src/store/navigation/navigation-action.ts | 2 + src/store/store.ts | 9 +- src/store/workbench/workbench-actions.ts | 9 +- .../details-panel/collection-details.tsx | 4 +- .../collection-content-address-panel.tsx | 114 ++++++++++++++++++ src/views/favorite-panel/favorite-panel.tsx | 1 - src/views/workbench/workbench.tsx | 2 + 12 files changed, 261 insertions(+), 19 deletions(-) create mode 100644 src/store/collections-content-address-panel/collections-content-address-middleware-service.ts create mode 100644 src/store/collections-content-address-panel/collections-content-address-panel-actions.ts create mode 100644 src/views/collection-content-address-panel/collection-content-address-panel.tsx diff --git a/src/components/details-attribute/details-attribute.tsx b/src/components/details-attribute/details-attribute.tsx index d255d14b..b8955d4e 100644 --- a/src/components/details-attribute/details-attribute.tsx +++ b/src/components/details-attribute/details-attribute.tsx @@ -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; 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) => {label} - { link - ? {value} - : - {value} - {children} - } + {link && {value}} + {linkInsideCard && {value}} + {!link && !linkInsideCard && + {value} + {children} + + } + ); diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index 2811f95a..50ba319e 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -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 diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 3fd6670d..dd09d8ec 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -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 index 00000000..53c45d57 --- /dev/null +++ b/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts @@ -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) { + const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId()); + if (!dataExplorer) { + api.dispatch(collectionPanelDataExplorerIsNotSet()); + } else { + const sortColumn = getSortColumn(dataExplorer); + + const contentOrder = new OrderBuilder(); + + 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 index 00000000..11f1a8cc --- /dev/null +++ b/src/store/collections-content-address-panel/collections-content-address-panel-actions.ts @@ -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()); + }; diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index af7a0c03..d7ad0178 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -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/store.ts b/src/store/store.ts index 9d7dcdd4..eff2454a 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -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); diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index 378bb8de..25852cbf 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -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(initSidePanelTree()); if (router.location) { @@ -153,6 +155,11 @@ export const loadFavorites = () => dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES)); }); +export const loadCollectionContentAddress = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + 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 => { diff --git a/src/views-components/details-panel/collection-details.tsx b/src/views-components/details-panel/collection-details.tsx index 01b867b3..b47e9edf 100644 --- a/src/views-components/details-panel/collection-details.tsx +++ b/src/views-components/details-panel/collection-details.tsx @@ -25,8 +25,8 @@ export class CollectionDetails extends DetailsData { {/* Links but we dont have view */} - - + + {/* Missing attrs */} 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 index 00000000..009e16c7 --- /dev/null +++ b/src/views/collection-content-address-panel/collection-content-address-panel.tsx @@ -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 = (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, uuid: string) => void; + onItemClick: (item: string) => void; + onItemDoubleClick: (item: string) => void; +} + +const mapDispatchToProps = (dispatch: Dispatch): CollectionContentAddressMainCardActionProps => ({ + onContextMenu: (event, resourceUuid) => { + const isAdmin = dispatch(getIsAdmin()); + const kind = resourceKindToContextMenuKind(resourceUuid, isAdmin); + if (kind) { + dispatch(openContextMenu(event, { + name: '', + uuid: resourceUuid, + ownerUuid: '', + kind: ResourceKind.NONE, + menuKind: kind + })); + } + dispatch(loadDetailsPanel(resourceUuid)); + }, + onItemClick: (uuid: string) => { + dispatch(loadDetailsPanel(uuid)); + }, + onItemDoubleClick: uuid => { + dispatch(navigateTo(uuid)); + } +}); + +export const CollectionsContentAddressPanel = withStyles(styles)( + connect(null, mapDispatchToProps)( + class extends React.Component> { + render() { + return + {/* + + Back test + */} + + } />; + ; + } + } + ) +); \ No newline at end of file diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx index 8498b787..e7234009 100644 --- a/src/views/favorite-panel/favorite-panel.tsx +++ b/src/views/favorite-panel/favorite-panel.tsx @@ -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'; diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index a31c7d25..e3668373 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -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 = + -- 2.30.2