From: Pawel Kowalczyk Date: Wed, 27 Mar 2019 13:37:35 +0000 (+0100) Subject: data-explorer-routing-and-admins-context-menu-for-resources X-Git-Tag: 1.4.0~25^2~6 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/80fdf9603c057f1e41915e7c8890c942d240b36e data-explorer-routing-and-admins-context-menu-for-resources Feature #14941 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- diff --git a/src/index.tsx b/src/index.tsx index cfaff70a..9f9b27ca 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -61,6 +61,9 @@ import { groupActionSet } from '~/views-components/context-menu/action-sets/grou import { groupMemberActionSet } from '~/views-components/context-menu/action-sets/group-member-action-set'; import { linkActionSet } from '~/views-components/context-menu/action-sets/link-action-set'; import { loadFileViewersConfig } from '~/store/file-viewers/file-viewers-actions'; +import { collectionAdminActionSet } from '~/views-components/context-menu/action-sets/collection-admin-action-set'; +import { processResourceAdminActionSet } from '~/views-components/context-menu/action-sets/process-resource-admin-action-set'; +import { projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set'; console.log(`Starting arvados [${getBuildInfo()}]`); @@ -87,6 +90,9 @@ addMenuActionSet(ContextMenuKind.NODE, computeNodeActionSet); addMenuActionSet(ContextMenuKind.API_CLIENT_AUTHORIZATION, apiClientAuthorizationActionSet); addMenuActionSet(ContextMenuKind.GROUPS, groupActionSet); addMenuActionSet(ContextMenuKind.GROUP_MEMBER, groupMemberActionSet); +addMenuActionSet(ContextMenuKind.COLLECTION_ADMIN, collectionAdminActionSet); +addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet); +addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet); fetchConfig() .then(({ config, apiHost }) => { diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts index 141ae20b..2811f95a 100644 --- a/src/routes/route-change-handlers.ts +++ b/src/routes/route-change-handlers.ts @@ -23,6 +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 trashMatch = Routes.matchTrashRoute(pathname); const processMatch = Routes.matchProcessRoute(pathname); const processLogMatch = Routes.matchProcessLogRoute(pathname); @@ -55,6 +56,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => { store.dispatch(WorkbenchActions.loadCollection(collectionMatch.params.id)); } else if (favoriteMatch) { store.dispatch(WorkbenchActions.loadFavorites()); + } else if (publicFavoritesMatch) { + store.dispatch(WorkbenchActions.loadPublicFavorites()); } else if (trashMatch) { store.dispatch(WorkbenchActions.loadTrash()); } else if (processMatch) { diff --git a/src/routes/routes.ts b/src/routes/routes.ts index b1da9496..3fd6670d 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -33,7 +33,8 @@ export const Routes = { API_CLIENT_AUTHORIZATIONS: `/api_client_authorizations`, GROUPS: '/groups', GROUP_DETAILS: `/group/:id(${RESOURCE_UUID_PATTERN})`, - LINKS: '/links' + LINKS: '/links', + PUBLIC_FAVORITES: '/public-favorites' }; export const getResourceUrl = (uuid: string) => { @@ -131,6 +132,9 @@ export const matchGroupsRoute = (route: string) => export const matchGroupDetailsRoute = (route: string) => matchPath(route, { path: Routes.GROUP_DETAILS }); - + export const matchLinksRoute = (route: string) => matchPath(route, { path: Routes.LINKS }); + +export const matchPublicFavorites = (route: string) => + matchPath(route, { path: Routes.PUBLIC_FAVORITES }); diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts index 269495e5..121ea50b 100644 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@ -194,15 +194,15 @@ export const openProcessContextMenu = (event: React.MouseEvent, pro } }; -export const resourceKindToContextMenuKind = (uuid: string) => { +export const resourceKindToContextMenuKind = (uuid: string, isAdmin?: boolean) => { const kind = extractUuidKind(uuid); switch (kind) { case ResourceKind.PROJECT: - return ContextMenuKind.PROJECT; + return !isAdmin ? ContextMenuKind.PROJECT : ContextMenuKind.PROJECT_ADMIN; case ResourceKind.COLLECTION: - return ContextMenuKind.COLLECTION_RESOURCE; + return !isAdmin ? ContextMenuKind.COLLECTION_RESOURCE : ContextMenuKind.COLLECTION_ADMIN; case ResourceKind.PROCESS: - return ContextMenuKind.PROCESS_RESOURCE; + return !isAdmin ? ContextMenuKind.PROCESS_RESOURCE : ContextMenuKind.PROCESS_ADMIN; case ResourceKind.USER: return ContextMenuKind.ROOT_PROJECT; case ResourceKind.LINK: diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts index f610eb5e..af7a0c03 100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@ -27,6 +27,8 @@ export const navigateTo = (uuid: string) => } if (uuid === SidePanelTreeCategory.FAVORITES) { dispatch(navigateToFavorites); + } else if (uuid === SidePanelTreeCategory.PUBLIC_FAVORITES) { + dispatch(navigateToPublicFavorites); } else if (uuid === SidePanelTreeCategory.SHARED_WITH_ME) { dispatch(navigateToSharedWithMe); } else if (uuid === SidePanelTreeCategory.WORKFLOWS) { @@ -44,6 +46,8 @@ export const navigateToFavorites = push(Routes.FAVORITES); export const navigateToTrash = push(Routes.TRASH); +export const navigateToPublicFavorites = push(Routes.PUBLIC_FAVORITES); + export const navigateToWorkflows = push(Routes.WORKFLOWS); export const navigateToProject = compose(push, getProjectUrl); diff --git a/src/store/public-favorites-panel/public-favorites-action.ts b/src/store/public-favorites-panel/public-favorites-action.ts new file mode 100644 index 00000000..ae9f8e8f --- /dev/null +++ b/src/store/public-favorites-panel/public-favorites-action.ts @@ -0,0 +1,10 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { bindDataExplorerActions } from "~/store/data-explorer/data-explorer-action"; + +export const PUBLIC_FAVORITE_PANEL_ID = "publicFavoritePanel"; +export const publicFavoritePanelActions = bindDataExplorerActions(PUBLIC_FAVORITE_PANEL_ID); + +export const loadPublicFavoritePanel = () => publicFavoritePanelActions.REQUEST_ITEMS(); \ No newline at end of file diff --git a/src/store/public-favorites-panel/public-favorites-middleware-service.ts b/src/store/public-favorites-panel/public-favorites-middleware-service.ts new file mode 100644 index 00000000..4e68ddc6 --- /dev/null +++ b/src/store/public-favorites-panel/public-favorites-middleware-service.ts @@ -0,0 +1,103 @@ +// 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, getDataExplorerColumnFilters } 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 { publicFavoritePanelActions } from '~/store/public-favorites-panel/public-favorites-action'; +import { DataColumns } from '~/components/data-table/data-table'; +import { serializeSimpleObjectTypeFilters } from '../resource-type-filters/resource-type-filters'; +import { LinkResource } from '~/models/link'; +import { GroupContentsResource, GroupContentsResourcePrefix } from '~/services/groups-service/groups-service'; +import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions'; +import { loadMissingProcessesInformation } from '~/store/project-panel/project-panel-middleware-service'; +import { updateFavorites } from '~/store/favorites/favorites-actions'; + +export class PublicFavoritesMiddlewareService 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(favoritesPanelDataExplorerIsNotSet()); + } else { + const columns = dataExplorer.columns as DataColumns; + const sortColumn = getSortColumn(dataExplorer); + const typeFilters = serializeSimpleObjectTypeFilters(getDataExplorerColumnFilters(columns, FavoritePanelColumnNames.TYPE)); + + + const linkOrder = new OrderBuilder(); + const contentOrder = new OrderBuilder(); + + if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) { + const direction = sortColumn.sortDirection === SortDirection.ASC + ? OrderDirection.ASC + : OrderDirection.DESC; + + linkOrder.addOrder(direction, "name"); + contentOrder + .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION) + .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS) + .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT); + } + try { + api.dispatch(progressIndicatorActions.START_WORKING(this.getId())); + const response = await this.services.favoriteService + .list(this.services.authService.getUuid()!, { + limit: dataExplorer.rowsPerPage, + offset: dataExplorer.page * dataExplorer.rowsPerPage, + linkOrder: linkOrder.getOrder(), + contentOrder: contentOrder.getOrder(), + filters: new FilterBuilder() + .addILike("name", dataExplorer.searchValue) + .addIsA("headUuid", typeFilters) + .getFilters(), + + }); + api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); + api.dispatch(resourcesActions.SET_RESOURCES(response.items)); + await api.dispatch(loadMissingProcessesInformation(response.items)); + api.dispatch(publicFavoritePanelActions.SET_ITEMS({ + items: response.items.map(resource => resource.uuid), + itemsAvailable: response.itemsAvailable, + page: Math.floor(response.offset / response.limit), + rowsPerPage: response.limit + })); + api.dispatch(updateFavorites(response.items.map(item => item.uuid))); + } catch (e) { + api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); + api.dispatch(publicFavoritePanelActions.SET_ITEMS({ + items: [], + itemsAvailable: 0, + page: 0, + rowsPerPage: dataExplorer.rowsPerPage + })); + api.dispatch(couldNotFetchPublicFavorites()); + } + } + } +} + +const favoritesPanelDataExplorerIsNotSet = () => + snackbarActions.OPEN_SNACKBAR({ + message: 'Favorites panel is not ready.', + kind: SnackbarKind.ERROR + }); + +const couldNotFetchPublicFavorites = () => + snackbarActions.OPEN_SNACKBAR({ + message: 'Could not fetch public favorites contents.', + kind: SnackbarKind.ERROR + }); \ No newline at end of file diff --git a/src/store/side-panel-tree/side-panel-tree-actions.ts b/src/store/side-panel-tree/side-panel-tree-actions.ts index 6032666f..6ad71391 100644 --- a/src/store/side-panel-tree/side-panel-tree-actions.ts +++ b/src/store/side-panel-tree/side-panel-tree-actions.ts @@ -19,6 +19,7 @@ import { GroupClass } from '~/models/group'; export enum SidePanelTreeCategory { PROJECTS = 'Projects', SHARED_WITH_ME = 'Shared with me', + PUBLIC_FAVORITES = 'Public Favorites', WORKFLOWS = 'Workflows', FAVORITES = 'Favorites', TRASH = 'Trash' @@ -42,6 +43,7 @@ export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker) }; const SIDE_PANEL_CATEGORIES = [ + SidePanelTreeCategory.PUBLIC_FAVORITES, SidePanelTreeCategory.WORKFLOWS, SidePanelTreeCategory.FAVORITES, SidePanelTreeCategory.TRASH, diff --git a/src/store/side-panel/side-panel-action.ts b/src/store/side-panel/side-panel-action.ts index 6d3e4746..f5ec5efc 100644 --- a/src/store/side-panel/side-panel-action.ts +++ b/src/store/side-panel/side-panel-action.ts @@ -4,7 +4,7 @@ import { Dispatch } from 'redux'; import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions'; -import { navigateToFavorites, navigateTo, navigateToTrash, navigateToSharedWithMe, navigateToWorkflows } from '../navigation/navigation-action'; +import { navigateToFavorites, navigateTo, navigateToTrash, navigateToSharedWithMe, navigateToWorkflows, navigateToPublicFavorites } from '~/store/navigation/navigation-action'; import {snackbarActions, SnackbarKind} from '~/store/snackbar/snackbar-actions'; export const navigateFromSidePanel = (id: string) => @@ -20,6 +20,8 @@ const getSidePanelTreeCategoryAction = (id: string) => { switch (id) { case SidePanelTreeCategory.FAVORITES: return navigateToFavorites; + case SidePanelTreeCategory.PUBLIC_FAVORITES: + return navigateToPublicFavorites; case SidePanelTreeCategory.TRASH: return navigateToTrash; case SidePanelTreeCategory.SHARED_WITH_ME: diff --git a/src/store/store.ts b/src/store/store.ts index 04813b1c..bdebebab 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -59,6 +59,8 @@ import { COMPUTE_NODE_PANEL_ID } from '~/store/compute-nodes/compute-nodes-actio import { ComputeNodeMiddlewareService } from '~/store/compute-nodes/compute-nodes-middleware-service'; import { API_CLIENT_AUTHORIZATION_PANEL_ID } from '~/store/api-client-authorizations/api-client-authorizations-actions'; import { ApiClientAuthorizationMiddlewareService } from '~/store/api-client-authorizations/api-client-authorizations-middleware-service'; +import { PublicFavoritesMiddlewareService } from '~/store/public-favorites-panel/public-favorites-middleware-service'; +import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action'; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -109,6 +111,9 @@ export function configureStore(history: History, services: ServiceRepository): R const apiClientAuthorizationMiddlewareService = dataExplorerMiddleware( new ApiClientAuthorizationMiddlewareService(services, API_CLIENT_AUTHORIZATION_PANEL_ID) ); + const publicFavoritesMiddleware = dataExplorerMiddleware( + new PublicFavoritesMiddlewareService(services, PUBLIC_FAVORITE_PANEL_ID) + ); const middlewares: Middleware[] = [ routerMiddleware(history), thunkMiddleware.withExtraArgument(services), @@ -123,7 +128,8 @@ export function configureStore(history: History, services: ServiceRepository): R groupDetailsPanelMiddleware, linkPanelMiddleware, computeNodeMiddleware, - apiClientAuthorizationMiddlewareService + apiClientAuthorizationMiddlewareService, + publicFavoritesMiddleware ]; 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 ddfe296c..e2276cef 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -92,6 +92,7 @@ import { groupsPanelColumns } from '~/views/groups-panel/groups-panel'; import * as groupDetailsPanelActions from '~/store/group-details-panel/group-details-panel-actions'; import { groupDetailsPanelColumns } from '~/views/group-details-panel/group-details-panel'; import { DataTableFetchMode } from "~/components/data-table/data-table"; +import { loadPublicFavoritePanel, publicFavoritePanelActions } from '~/store/public-favorites-panel/public-favorites-action'; export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen'; @@ -119,6 +120,7 @@ export const loadWorkbench = () => if (user) { dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns })); dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns })); + dispatch(publicFavoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns })); dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns })); dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns })); dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns })); @@ -437,6 +439,14 @@ export const loadWorkflow = handleFirstTimeLoad(async (dispatch: Dispatch) dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.WORKFLOWS)); }); +export const loadPublicFavorites = () => + handleFirstTimeLoad( + (dispatch: Dispatch) => { + dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.PUBLIC_FAVORITES)); + dispatch(loadPublicFavoritePanel()); + dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.PUBLIC_FAVORITES)); + }); + export const loadSearchResults = handleFirstTimeLoad( async (dispatch: Dispatch) => { await dispatch(loadSearchResultsPanel()); diff --git a/src/views-components/context-menu/action-sets/collection-action-set.ts b/src/views-components/context-menu/action-sets/collection-action-set.ts index d6095d10..fe77b749 100644 --- a/src/views-components/context-menu/action-sets/collection-action-set.ts +++ b/src/views-components/context-menu/action-sets/collection-action-set.ts @@ -12,7 +12,6 @@ import { openMoveCollectionDialog } from '~/store/collections/collection-move-ac import { openCollectionCopyDialog } from "~/store/collections/collection-copy-actions"; import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action"; import { toggleCollectionTrashed } from "~/store/trash/trash-actions"; -import { detailsPanelActions } from '~/store/details-panel/details-panel-action'; import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions'; import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab"; import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action'; diff --git a/src/views-components/context-menu/action-sets/collection-admin-action-set.ts b/src/views-components/context-menu/action-sets/collection-admin-action-set.ts new file mode 100644 index 00000000..6ba86381 --- /dev/null +++ b/src/views-components/context-menu/action-sets/collection-admin-action-set.ts @@ -0,0 +1,98 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { ContextMenuActionSet } from "../context-menu-action-set"; +import { ToggleFavoriteAction } from "../actions/favorite-action"; +import { toggleFavorite } from "~/store/favorites/favorites-actions"; +import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, AdvancedIcon } from "~/components/icon/icon"; +import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions"; +import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action"; +import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions'; +import { openCollectionCopyDialog } from "~/store/collections/collection-copy-actions"; +import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action"; +import { toggleCollectionTrashed } from "~/store/trash/trash-actions"; +import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions'; +import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab"; +import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action'; +import { TogglePublicFavoriteAction } from "~/views-components/context-menu/actions/public-favorite-action"; + +export const collectionAdminActionSet: ContextMenuActionSet = [[ + { + icon: RenameIcon, + name: "Edit collection", + execute: (dispatch, resource) => { + dispatch(openCollectionUpdateDialog(resource)); + } + }, + { + icon: ShareIcon, + name: "Share", + execute: (dispatch, { uuid }) => { + dispatch(openSharingDialog(uuid)); + } + }, + { + component: ToggleFavoriteAction, + execute: (dispatch, resource) => { + dispatch(toggleFavorite(resource)).then(() => { + dispatch(favoritePanelActions.REQUEST_ITEMS()); + }); + } + }, + { + component: TogglePublicFavoriteAction, + execute: (dispatch, resource) => { + dispatch(toggleFavorite(resource)).then(() => { + dispatch(favoritePanelActions.REQUEST_ITEMS()); + }); + } + }, + { + icon: MoveToIcon, + name: "Move to", + execute: (dispatch, resource) => dispatch(openMoveCollectionDialog(resource)) + }, + { + icon: CopyIcon, + name: "Copy to project", + execute: (dispatch, resource) => { + dispatch(openCollectionCopyDialog(resource)); + } + + }, + { + icon: DetailsIcon, + name: "View details", + execute: dispatch => { + dispatch(toggleDetailsPanel()); + } + }, + // { + // icon: ProvenanceGraphIcon, + // name: "Provenance graph", + // execute: (dispatch, resource) => { + // // add code + // } + // }, + { + icon: AdvancedIcon, + name: "Advanced", + execute: (dispatch, resource) => { + dispatch(openAdvancedTabDialog(resource.uuid)); + } + }, + { + component: ToggleTrashAction, + execute: (dispatch, resource) => { + dispatch(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!)); + } + }, + // { + // icon: RemoveIcon, + // name: "Remove", + // execute: (dispatch, resource) => { + // // add code + // } + // } +]]; diff --git a/src/views-components/context-menu/action-sets/process-resource-admin-action-set.ts b/src/views-components/context-menu/action-sets/process-resource-admin-action-set.ts new file mode 100644 index 00000000..e0cb796b --- /dev/null +++ b/src/views-components/context-menu/action-sets/process-resource-admin-action-set.ts @@ -0,0 +1,77 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { ContextMenuActionSet } from "../context-menu-action-set"; +import { ToggleFavoriteAction } from "../actions/favorite-action"; +import { toggleFavorite } from "~/store/favorites/favorites-actions"; +import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon"; +import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action"; +import { openMoveProcessDialog } from '~/store/processes/process-move-actions'; +import { openProcessUpdateDialog } from "~/store/processes/process-update-actions"; +import { openCopyProcessDialog } from '~/store/processes/process-copy-actions'; +import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions"; +import { openRemoveProcessDialog } from "~/store/processes/processes-actions"; +import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action'; +import { TogglePublicFavoriteAction } from "~/views-components/context-menu/actions/public-favorite-action"; + +export const processResourceAdminActionSet: ContextMenuActionSet = [[ + { + icon: RenameIcon, + name: "Edit process", + execute: (dispatch, resource) => { + dispatch(openProcessUpdateDialog(resource)); + } + }, + { + icon: ShareIcon, + name: "Share", + execute: (dispatch, { uuid }) => { + dispatch(openSharingDialog(uuid)); + } + }, + { + component: ToggleFavoriteAction, + execute: (dispatch, resource) => { + dispatch(toggleFavorite(resource)).then(() => { + dispatch(favoritePanelActions.REQUEST_ITEMS()); + }); + } + }, + { + component: TogglePublicFavoriteAction, + execute: (dispatch, resource) => { + dispatch(toggleFavorite(resource)).then(() => { + dispatch(favoritePanelActions.REQUEST_ITEMS()); + }); + } + }, + { + icon: MoveToIcon, + name: "Move to", + execute: (dispatch, resource) => { + dispatch(openMoveProcessDialog(resource)); + } + }, + { + icon: CopyIcon, + name: "Copy to project", + execute: (dispatch, resource) => { + dispatch(openCopyProcessDialog(resource)); + } + }, + { + icon: DetailsIcon, + name: "View details", + execute: dispatch => { + dispatch(toggleDetailsPanel()); + } + }, + { + name: "Remove", + icon: RemoveIcon, + execute: (dispatch, resource) => { + dispatch(openRemoveProcessDialog(resource.uuid)); + } + } +]]; diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts index 660d7ea0..32616fce 100644 --- a/src/views-components/context-menu/action-sets/project-action-set.ts +++ b/src/views-components/context-menu/action-sets/project-action-set.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import { ContextMenuActionSet } from "../context-menu-action-set"; -import { NewProjectIcon, RenameIcon, CopyIcon, MoveToIcon, DetailsIcon, AdvancedIcon } from '~/components/icon/icon'; +import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon } from '~/components/icon/icon'; import { ToggleFavoriteAction } from "../actions/favorite-action"; import { toggleFavorite } from "~/store/favorites/favorites-actions"; import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action"; diff --git a/src/views-components/context-menu/action-sets/project-admin-action-set.ts b/src/views-components/context-menu/action-sets/project-admin-action-set.ts new file mode 100644 index 00000000..1076696a --- /dev/null +++ b/src/views-components/context-menu/action-sets/project-admin-action-set.ts @@ -0,0 +1,93 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { ContextMenuActionSet } from "../context-menu-action-set"; +import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon } from '~/components/icon/icon'; +import { ToggleFavoriteAction } from "../actions/favorite-action"; +import { toggleFavorite } from "~/store/favorites/favorites-actions"; +import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action"; +import { openMoveProjectDialog } from '~/store/projects/project-move-actions'; +import { openProjectCreateDialog } from '~/store/projects/project-create-actions'; +import { openProjectUpdateDialog } from '~/store/projects/project-update-actions'; +import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action"; +import { toggleProjectTrashed } from "~/store/trash/trash-actions"; +import { ShareIcon } from '~/components/icon/icon'; +import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions"; +import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab"; +import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action'; +import { TogglePublicFavoriteAction } from "~/views-components/context-menu/actions/public-favorite-action"; + +export const projectAdminActionSet: ContextMenuActionSet = [[ + { + icon: NewProjectIcon, + name: "New project", + execute: (dispatch, resource) => { + dispatch(openProjectCreateDialog(resource.uuid)); + } + }, + { + icon: RenameIcon, + name: "Edit project", + execute: (dispatch, resource) => { + dispatch(openProjectUpdateDialog(resource)); + } + }, + { + icon: ShareIcon, + name: "Share", + execute: (dispatch, { uuid }) => { + dispatch(openSharingDialog(uuid)); + } + }, + { + component: ToggleFavoriteAction, + execute: (dispatch, resource) => { + dispatch(toggleFavorite(resource)).then(() => { + dispatch(favoritePanelActions.REQUEST_ITEMS()); + }); + } + }, + { + component: TogglePublicFavoriteAction, + execute: (dispatch, resource) => { + dispatch(toggleFavorite(resource)).then(() => { + dispatch(favoritePanelActions.REQUEST_ITEMS()); + }); + } + }, + { + icon: MoveToIcon, + name: "Move to", + execute: (dispatch, resource) => { + dispatch(openMoveProjectDialog(resource)); + } + }, + // { + // icon: CopyIcon, + // name: "Copy to project", + // execute: (dispatch, resource) => { + // // add code + // } + // }, + { + icon: DetailsIcon, + name: "View details", + execute: dispatch => { + dispatch(toggleDetailsPanel()); + } + }, + { + icon: AdvancedIcon, + name: "Advanced", + execute: (dispatch, resource) => { + dispatch(openAdvancedTabDialog(resource.uuid)); + } + }, + { + component: ToggleTrashAction, + execute: (dispatch, resource) => { + dispatch(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!)); + } + }, +]]; diff --git a/src/views-components/context-menu/actions/public-favorite-action.tsx b/src/views-components/context-menu/actions/public-favorite-action.tsx new file mode 100644 index 00000000..647b33b5 --- /dev/null +++ b/src/views-components/context-menu/actions/public-favorite-action.tsx @@ -0,0 +1,30 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from "react"; +import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core"; +import { AddFavoriteIcon, RemoveFavoriteIcon } from "~/components/icon/icon"; +import { connect } from "react-redux"; +import { RootState } from "~/store/store"; + +const mapStateToProps = (state: RootState, props: { onClick: () => {} }) => ({ + isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true, + onClick: props.onClick +}); + +export const TogglePublicFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean, onClick: () => void }) => + + + {props.isFavorite + ? + : } + + + {props.isFavorite + ? <>Remove from public favorites + : <>Add to public favorites} + + ); diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index f6910290..65e98cc5 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -65,6 +65,7 @@ export enum ContextMenuKind { API_CLIENT_AUTHORIZATION = "ApiClientAuthorization", ROOT_PROJECT = "RootProject", PROJECT = "Project", + PROJECT_ADMIN = "ProjectAdmin", RESOURCE = "Resource", FAVORITE = "Favorite", TRASH = "Trash", @@ -72,9 +73,11 @@ export enum ContextMenuKind { COLLECTION_FILES_ITEM = "CollectionFilesItem", COLLECTION_FILES_NOT_SELECTED = "CollectionFilesNotSelected", COLLECTION = 'Collection', + COLLECTION_ADMIN = 'CollectionAdmin', COLLECTION_RESOURCE = 'CollectionResource', TRASHED_COLLECTION = 'TrashedCollection', PROCESS = "Process", + PROCESS_ADMIN = 'ProcessAdmin', PROCESS_RESOURCE = 'ProcessResource', PROCESS_LOGS = "ProcessLogs", REPOSITORY = "Repository", diff --git a/src/views-components/data-explorer/data-explorer.tsx b/src/views-components/data-explorer/data-explorer.tsx index ed4bffd6..371569d1 100644 --- a/src/views-components/data-explorer/data-explorer.tsx +++ b/src/views-components/data-explorer/data-explorer.tsx @@ -15,7 +15,7 @@ import { DataTableFilters } from '~/components/data-table-filters/data-table-fil interface Props { id: string; onRowClick: (item: any) => void; - onContextMenu?: (event: React.MouseEvent, item: any) => void; + onContextMenu?: (event: React.MouseEvent, item: any, isAdmin?: boolean) => void; onRowDoubleClick: (item: any) => void; extractKey?: (item: any) => React.Key; } diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index 825bab30..2483c516 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -113,6 +113,7 @@ const DEFAUL_VIEW_MESSAGES = [ interface ProjectPanelDataProps { currentItemId: string; resources: ResourcesState; + isAdmin: boolean; } type ProjectPanelProps = ProjectPanelDataProps & DispatchProp @@ -121,7 +122,8 @@ type ProjectPanelProps = ProjectPanelDataProps & DispatchProp export const ProjectPanel = withStyles(styles)( connect((state: RootState) => ({ currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties), - resources: state.resources + resources: state.resources, + isAdmin: state.auth.user!.isAdmin }))( class extends React.Component { render() { @@ -146,7 +148,7 @@ export const ProjectPanel = withStyles(styles)( } handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { - const menuKind = resourceKindToContextMenuKind(resourceUuid); + const menuKind = resourceKindToContextMenuKind(resourceUuid, this.props.isAdmin); const resource = getResource(resourceUuid)(this.props.resources); if (menuKind && resource) { this.props.dispatch(openContextMenu(event, { diff --git a/src/views/public-favorites-panel/public-favorites-panel.tsx b/src/views/public-favorites-panel/public-favorites-panel.tsx new file mode 100644 index 00000000..f559a616 --- /dev/null +++ b/src/views/public-favorites-panel/public-favorites-panel.tsx @@ -0,0 +1,167 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; +import { DataExplorer } from "~/views-components/data-explorer/data-explorer"; +import { connect, DispatchProp } from 'react-redux'; +import { DataColumns } from '~/components/data-table/data-table'; +import { RouteComponentProps } from 'react-router'; +import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters'; +import { SortDirection } from '~/components/data-table/data-column'; +import { ResourceKind } from '~/models/resource'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { + ProcessStatus, + ResourceFileSize, + ResourceLastModifiedDate, + ResourceName, + ResourceOwner, + 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 { ContainerRequestState } from "~/models/container-request"; +import { FavoritesState } from '~/store/favorites/favorites-reducer'; +import { RootState } from '~/store/store'; +import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view'; +import { createTree } from '~/models/tree'; +import { getSimpleObjectTypeFilters } from '~/store/resource-type-filters/resource-type-filters'; +import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action'; + +type CssRules = "toolbar" | "button"; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + toolbar: { + paddingBottom: theme.spacing.unit * 3, + textAlign: "right" + }, + button: { + marginLeft: theme.spacing.unit + }, +}); + +export enum FavoritePanelColumnNames { + NAME = "Name", + STATUS = "Status", + TYPE = "Type", + OWNER = "Owner", + FILE_SIZE = "File size", + LAST_MODIFIED = "Last modified" +} + +export interface FavoritePanelFilter extends DataTableFilterItem { + type: ResourceKind | ContainerRequestState; +} + +export const favoritePanelColumns: DataColumns = [ + { + name: FavoritePanelColumnNames.NAME, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: "Status", + selected: true, + configurable: true, + filters: createTree(), + render: uuid => + }, + { + name: FavoritePanelColumnNames.TYPE, + selected: true, + configurable: true, + filters: getSimpleObjectTypeFilters(), + render: uuid => + }, + { + name: FavoritePanelColumnNames.OWNER, + selected: true, + configurable: true, + filters: createTree(), + render: uuid => + }, + { + name: FavoritePanelColumnNames.FILE_SIZE, + selected: true, + configurable: true, + filters: createTree(), + render: uuid => + }, + { + name: FavoritePanelColumnNames.LAST_MODIFIED, + selected: true, + configurable: true, + sortDirection: SortDirection.DESC, + filters: createTree(), + render: uuid => + } +]; + +interface FavoritePanelDataProps { + favorites: FavoritesState; +} + +interface FavoritePanelActionProps { + onItemClick: (item: string) => void; + onContextMenu: (event: React.MouseEvent, item: string) => void; + onDialogOpen: (ownerUuid: string) => void; + onItemDoubleClick: (item: string) => void; +} +const mapStateToProps = ({ favorites }: RootState): FavoritePanelDataProps => ({ + favorites +}); + +const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({ + onContextMenu: (event, resourceUuid) => { + const kind = resourceKindToContextMenuKind(resourceUuid); + if (kind) { + dispatch(openContextMenu(event, { + name: '', + uuid: resourceUuid, + ownerUuid: '', + kind: ResourceKind.NONE, + menuKind: kind + })); + } + dispatch(loadDetailsPanel(resourceUuid)); + }, + onDialogOpen: (ownerUuid: string) => { return; }, + onItemClick: (resourceUuid: string) => { + dispatch(loadDetailsPanel(resourceUuid)); + }, + onItemDoubleClick: uuid => { + dispatch(navigateTo(uuid)); + } +}); + +type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp + & WithStyles & RouteComponentProps<{ id: string }>; + +export const PublicFavoritePanel = withStyles(styles)( + connect(mapStateToProps, mapDispatchToProps)( + class extends React.Component { + render() { + return + } />; + } + } + ) +); diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index a009d614..a31c7d25 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -91,6 +91,7 @@ import { RemoveGroupMemberDialog } from '~/views-components/groups-dialog/member import { GroupMemberAttributesDialog } from '~/views-components/groups-dialog/member-attributes-dialog'; 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'; type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content'; @@ -138,9 +139,9 @@ export const WorkbenchPanel = + primaryIndex={0} primaryMinSize={10} + secondaryInitialSize={getSplitterInitialSize()} secondaryMinSize={40} + onSecondaryPaneSizeChange={saveSplitterSize}> @@ -174,6 +175,7 @@ export const WorkbenchPanel = +