From efe0283919eb18e60ad876eaf6edef03c6cf04b3 Mon Sep 17 00:00:00 2001 From: Daniel Kos Date: Tue, 21 Aug 2018 10:17:44 +0200 Subject: [PATCH] Add trash view Feature #13828 Arvados-DCO-1.1-Signed-off-by: Daniel Kos --- src/common/formatters.ts | 11 +- src/components/data-table/data-table.tsx | 2 +- src/models/collection.ts | 9 +- src/models/container-request.ts | 38 ----- src/models/group.ts | 9 +- src/models/process.ts | 35 +++- src/models/project.ts | 2 +- src/models/resource.ts | 7 +- src/models/test-utils.ts | 2 +- src/services/groups-service/groups-service.ts | 1 + src/services/services.ts | 7 +- .../trash-service/trash-service.test.ts | 17 ++ src/services/trash-service/trash-service.ts | 12 ++ src/store/side-panel/side-panel-reducer.ts | 11 +- src/store/store.ts | 8 +- src/store/trash-panel/trash-panel-action.ts | 8 + .../trash-panel-middleware-service.ts | 80 ++++++++++ .../data-explorer/renderers.tsx | 2 +- src/views/favorite-panel/favorite-panel.tsx | 16 +- src/views/project-panel/project-panel.tsx | 16 +- src/views/trash-panel/trash-panel-item.ts | 27 ++++ src/views/trash-panel/trash-panel.tsx | 149 ++++++++++++++++++ src/views/workbench/workbench.tsx | 30 ++++ 23 files changed, 416 insertions(+), 83 deletions(-) delete mode 100644 src/models/container-request.ts create mode 100644 src/services/trash-service/trash-service.test.ts create mode 100644 src/services/trash-service/trash-service.ts create mode 100644 src/store/trash-panel/trash-panel-action.ts create mode 100644 src/store/trash-panel/trash-panel-middleware-service.ts create mode 100644 src/views/trash-panel/trash-panel-item.ts create mode 100644 src/views/trash-panel/trash-panel.tsx diff --git a/src/common/formatters.ts b/src/common/formatters.ts index 49e0690515..b1baee7de9 100644 --- a/src/common/formatters.ts +++ b/src/common/formatters.ts @@ -2,10 +2,13 @@ // // SPDX-License-Identifier: AGPL-3.0 -export const formatDate = (isoDate: string) => { - const date = new Date(isoDate); - const text = date.toLocaleString(); - return text === 'Invalid Date' ? "" : text; +export const formatDate = (isoDate?: string) => { + if (isoDate) { + const date = new Date(isoDate); + const text = date.toLocaleString(); + return text === 'Invalid Date' ? "" : text; + } + return ""; }; export const formatFileSize = (size?: number) => { diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx index 34f8168a55..5a6f9e5a5b 100644 --- a/src/components/data-table/data-table.tsx +++ b/src/components/data-table/data-table.tsx @@ -63,7 +63,7 @@ export const DataTable = withStyles(styles)( return {renderHeader ? renderHeader() : - filters + filters.length > 0 ? diff --git a/src/models/collection.ts b/src/models/collection.ts index 0e96f7fd3d..5215998956 100644 --- a/src/models/collection.ts +++ b/src/models/collection.ts @@ -2,9 +2,9 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { Resource, ResourceKind } from "./resource"; +import { ResourceKind, TrashResource } from "./resource"; -export interface CollectionResource extends Resource { +export interface CollectionResource extends TrashResource { kind: ResourceKind.COLLECTION; name: string; description: string; @@ -14,11 +14,8 @@ export interface CollectionResource extends Resource { replicationDesired: number; replicationConfirmed: number; replicationConfirmedAt: string; - trashAt: string; - deleteAt: string; - isTrashed: boolean; } export const getCollectionUrl = (uuid: string) => { return `/collections/${uuid}`; -}; \ No newline at end of file +}; diff --git a/src/models/container-request.ts b/src/models/container-request.ts deleted file mode 100644 index d1bcc36c81..0000000000 --- a/src/models/container-request.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import { Resource, ResourceKind } from "./resource"; - -export enum ContainerRequestState { - UNCOMMITTED = "Uncommitted", - COMMITTED = "Committed", - FINAL = "Final" -} - -export interface ContainerRequestResource extends Resource { - kind: ResourceKind.CONTAINER_REQUEST; - name: string; - description: string; - properties: any; - state: ContainerRequestState; - requestingContainerUuid: string; - containerUuid: string; - containerCountMax: number; - mounts: any; - runtimeConstraints: any; - schedulingParameters: any; - containerImage: string; - environment: any; - cwd: string; - command: string[]; - outputPath: string; - outputName: string; - outputTtl: number; - priority: number; - expiresAt: string; - useExisting: boolean; - logUuid: string; - outputUuid: string; - filters: string; -} diff --git a/src/models/group.ts b/src/models/group.ts index 5e8d7a1e0b..5319250e1b 100644 --- a/src/models/group.ts +++ b/src/models/group.ts @@ -2,20 +2,17 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { Resource, ResourceKind } from "./resource"; +import { ResourceKind, TrashResource } from "./resource"; -export interface GroupResource extends Resource { +export interface GroupResource extends TrashResource { kind: ResourceKind.GROUP; name: string; groupClass: GroupClass | null; description: string; properties: string; writeableBy: string[]; - trashAt: string; - deleteAt: string; - isTrashed: boolean; } export enum GroupClass { PROJECT = "project" -} \ No newline at end of file +} diff --git a/src/models/process.ts b/src/models/process.ts index 1e04cb10f3..bcfbd3a557 100644 --- a/src/models/process.ts +++ b/src/models/process.ts @@ -2,6 +2,37 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { ContainerRequestResource } from "./container-request"; +import { Resource, ResourceKind } from "./resource"; -export type ProcessResource = ContainerRequestResource; +export enum ProcessState { + UNCOMMITTED = "Uncommitted", + COMMITTED = "Committed", + FINAL = "Final" +} + +export interface ProcessResource extends Resource { + kind: ResourceKind.PROCESS; + name: string; + description: string; + properties: any; + state: ProcessState; + requestingContainerUuid: string; + containerUuid: string; + containerCountMax: number; + mounts: any; + runtimeConstraints: any; + schedulingParameters: any; + containerImage: string; + environment: any; + cwd: string; + command: string[]; + outputPath: string; + outputName: string; + outputTtl: number; + priority: number; + expiresAt: string; + useExisting: boolean; + logUuid: string; + outputUuid: string; + filters: string; +} diff --git a/src/models/project.ts b/src/models/project.ts index b919450774..8e101ce29f 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { GroupResource, GroupClass } from "./group"; +import { GroupClass, GroupResource } from "./group"; export interface ProjectResource extends GroupResource { groupClass: GroupClass.PROJECT; diff --git a/src/models/resource.ts b/src/models/resource.ts index 6a76b07045..ab487da070 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -14,9 +14,14 @@ export interface Resource { etag: string; } +export interface TrashResource extends Resource { + trashAt: string; + deleteAt: string; + isTrashed: boolean; +} + export enum ResourceKind { COLLECTION = "arvados#collection", - CONTAINER_REQUEST = "arvados#containerRequest", GROUP = "arvados#group", PROCESS = "arvados#containerRequest", PROJECT = "arvados#group", diff --git a/src/models/test-utils.ts b/src/models/test-utils.ts index 6723430c34..49eea605d5 100644 --- a/src/models/test-utils.ts +++ b/src/models/test-utils.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { GroupResource, GroupClass } from "./group"; +import { GroupClass, GroupResource } from "./group"; import { Resource, ResourceKind } from "./resource"; import { ProjectResource } from "./project"; diff --git a/src/services/groups-service/groups-service.ts b/src/services/groups-service/groups-service.ts index 822c810ef7..39cc74a9c2 100644 --- a/src/services/groups-service/groups-service.ts +++ b/src/services/groups-service/groups-service.ts @@ -16,6 +16,7 @@ export interface ContentsArguments { order?: string; filters?: string; recursive?: boolean; + includeTrash?: boolean; } export type GroupContentsResource = diff --git a/src/services/services.ts b/src/services/services.ts index 61dd399206..91997552c0 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -12,8 +12,9 @@ import { CollectionService } from "./collection-service/collection-service"; import { TagService } from "./tag-service/tag-service"; import { CollectionFilesService } from "./collection-files-service/collection-files-service"; import { KeepService } from "./keep-service/keep-service"; -import { WebDAV } from "../common/webdav"; -import { Config } from "../common/config"; +import { WebDAV } from "~/common/webdav"; +import { Config } from "~/common/config"; +import { TrashService } from "~/services/trash-service/trash-service"; export type ServiceRepository = ReturnType; @@ -30,6 +31,7 @@ export const createServices = (config: Config) => { const projectService = new ProjectService(apiClient); const linkService = new LinkService(apiClient); const favoriteService = new FavoriteService(linkService, groupsService); + const trashService = new TrashService(apiClient); const collectionService = new CollectionService(apiClient, keepService, webdavClient, authService); const tagService = new TagService(linkService); const collectionFilesService = new CollectionFilesService(collectionService); @@ -43,6 +45,7 @@ export const createServices = (config: Config) => { projectService, linkService, favoriteService, + trashService, collectionService, tagService, collectionFilesService diff --git a/src/services/trash-service/trash-service.test.ts b/src/services/trash-service/trash-service.test.ts new file mode 100644 index 0000000000..f22d066a68 --- /dev/null +++ b/src/services/trash-service/trash-service.test.ts @@ -0,0 +1,17 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { GroupsService } from "../groups-service/groups-service"; +import { TrashService } from "./trash-service"; +import { mockResourceService } from "~/common/api/common-resource-service.test"; + +describe("TrashService", () => { + + let groupService: GroupsService; + + beforeEach(() => { + groupService = mockResourceService(GroupsService); + }); + +}); diff --git a/src/services/trash-service/trash-service.ts b/src/services/trash-service/trash-service.ts new file mode 100644 index 0000000000..fc02d2f19f --- /dev/null +++ b/src/services/trash-service/trash-service.ts @@ -0,0 +1,12 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { GroupsService } from "../groups-service/groups-service"; +import { AxiosInstance } from "axios"; + +export class TrashService extends GroupsService { + constructor(serverApi: AxiosInstance) { + super(serverApi); + } +} diff --git a/src/store/side-panel/side-panel-reducer.ts b/src/store/side-panel/side-panel-reducer.ts index db1cbe5de5..b68ce7a1c2 100644 --- a/src/store/side-panel/side-panel-reducer.ts +++ b/src/store/side-panel/side-panel-reducer.ts @@ -10,9 +10,11 @@ import { push } from "react-router-redux"; import { favoritePanelActions } from "../favorite-panel/favorite-panel-action"; import { projectPanelActions } from "../project-panel/project-panel-action"; import { projectActions } from "../project/project-action"; -import { getProjectUrl } from "../../models/project"; -import { columns as projectPanelColumns } from "../../views/project-panel/project-panel"; -import { columns as favoritePanelColumns } from "../../views/favorite-panel/favorite-panel"; +import { getProjectUrl } from "~/models/project"; +import { columns as projectPanelColumns } from "~/views/project-panel/project-panel"; +import { columns as favoritePanelColumns } from "~/views/favorite-panel/favorite-panel"; +import { columns as trashPanelColumns } from "~/views/trash-panel/trash-panel"; +import { trashPanelActions } from "~/store/trash-panel/trash-panel-action"; export type SidePanelState = SidePanelItem[]; @@ -102,6 +104,9 @@ export const sidePanelItems = [ active: false, activeAction: (dispatch: Dispatch) => { dispatch(push("/trash")); + dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns })); + dispatch(trashPanelActions.RESET_PAGINATION()); + dispatch(trashPanelActions.REQUEST_ITEMS()); } } ]; diff --git a/src/store/store.ts b/src/store/store.ts index a4bf9d6e3b..febaf933d0 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -29,6 +29,8 @@ import { CollectionsState, collectionsReducer } from './collections/collections- import { ServiceRepository } from "~/services/services"; import { treePickerReducer } from './tree-picker/tree-picker-reducer'; import { TreePicker } from './tree-picker/tree-picker'; +import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-middleware-service"; +import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action"; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -79,12 +81,16 @@ export function configureStore(history: History, services: ServiceRepository): R const favoritePanelMiddleware = dataExplorerMiddleware( new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID) ); + const trashPanelMiddleware = dataExplorerMiddleware( + new TrashPanelMiddlewareService(services, TRASH_PANEL_ID) + ); const middlewares: Middleware[] = [ routerMiddleware(history), thunkMiddleware.withExtraArgument(services), projectPanelMiddleware, - favoritePanelMiddleware + favoritePanelMiddleware, + trashPanelMiddleware ]; const enhancer = composeEnhancers(applyMiddleware(...middlewares)); return createStore(rootReducer, enhancer); diff --git a/src/store/trash-panel/trash-panel-action.ts b/src/store/trash-panel/trash-panel-action.ts new file mode 100644 index 0000000000..84d5602457 --- /dev/null +++ b/src/store/trash-panel/trash-panel-action.ts @@ -0,0 +1,8 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { bindDataExplorerActions } from "../data-explorer/data-explorer-action"; + +export const TRASH_PANEL_ID = "trashPanel"; +export const trashPanelActions = bindDataExplorerActions(TRASH_PANEL_ID); diff --git a/src/store/trash-panel/trash-panel-middleware-service.ts b/src/store/trash-panel/trash-panel-middleware-service.ts new file mode 100644 index 0000000000..2d1dbf7b90 --- /dev/null +++ b/src/store/trash-panel/trash-panel-middleware-service.ts @@ -0,0 +1,80 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service"; +import { RootState } from "../store"; +import { DataColumns } from "~/components/data-table/data-table"; +import { ServiceRepository } from "~/services/services"; +import { SortDirection } from "~/components/data-table/data-column"; +import { FilterBuilder } from "~/common/api/filter-builder"; +import { checkPresenceInFavorites } from "../favorites/favorites-actions"; +import { trashPanelActions } from "./trash-panel-action"; +import { Dispatch, MiddlewareAPI } from "redux"; +import { OrderBuilder, OrderDirection } from "~/common/api/order-builder"; +import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service"; +import { resourceToDataItem, TrashPanelItem } from "~/views/trash-panel/trash-panel-item"; +import { TrashPanelColumnNames, TrashPanelFilter } from "~/views/trash-panel/trash-panel"; +import { ProjectResource } from "~/models/project"; +import { ProjectPanelColumnNames } from "~/views/project-panel/project-panel"; + +export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService { + constructor(private services: ServiceRepository, id: string) { + super(id); + } + + requestItems(api: MiddlewareAPI) { + const dataExplorer = api.getState().dataExplorer[this.getId()]; + const columns = dataExplorer.columns as DataColumns; + const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE); + const typeFilters = this.getColumnFilters(columns, TrashPanelColumnNames.TYPE); + + const order = new OrderBuilder(); + + if (sortColumn) { + const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC + ? OrderDirection.ASC + : OrderDirection.DESC; + + const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt"; + order + .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION) + .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS) + .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT); + } + + const userUuid = this.services.authService.getUuid()!; + + this.services.trashService + .contents(userUuid, { + limit: dataExplorer.rowsPerPage, + offset: dataExplorer.page * dataExplorer.rowsPerPage, + order: order.getOrder(), + filters: new FilterBuilder() + .addIsA("uuid", typeFilters.map(f => f.type)) + .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION) + .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS) + .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT) + .getFilters(), + recursive: true, + includeTrash: true + }) + .then(response => { + api.dispatch(trashPanelActions.SET_ITEMS({ + items: response.items.map(resourceToDataItem).filter(it => it.isTrashed), + itemsAvailable: response.itemsAvailable, + page: Math.floor(response.offset / response.limit), + rowsPerPage: response.limit + })); + api.dispatch(checkPresenceInFavorites(response.items.map(item => item.uuid))); + }) + .catch(() => { + api.dispatch(trashPanelActions.SET_ITEMS({ + items: [], + itemsAvailable: 0, + page: 0, + rowsPerPage: dataExplorer.rowsPerPage + })); + }); + } +} diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 1b07642ab7..8246d02c37 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -42,7 +42,7 @@ export const renderIcon = (item: {kind: string}) => { } }; -export const renderDate = (date: string) => { +export const renderDate = (date?: string) => { return {formatDate(date)}; }; diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx index 125ea27ddf..49f1f4ab3a 100644 --- a/src/views/favorite-panel/favorite-panel.tsx +++ b/src/views/favorite-panel/favorite-panel.tsx @@ -11,7 +11,7 @@ import { DataColumns } from '~/components/data-table/data-table'; import { RouteComponentProps } from 'react-router'; import { RootState } from '~/store/store'; import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters'; -import { ContainerRequestState } from '~/models/container-request'; +import { ProcessState } from '~/models/process'; import { SortDirection } from '~/components/data-table/data-column'; import { ResourceKind } from '~/models/resource'; import { resourceLabel } from '~/common/labels'; @@ -42,7 +42,7 @@ export enum FavoritePanelColumnNames { } export interface FavoritePanelFilter extends DataTableFilterItem { - type: ResourceKind | ContainerRequestState; + type: ResourceKind | ProcessState; } export const columns: DataColumns = [ @@ -62,19 +62,19 @@ export const columns: DataColumns = [ sortDirection: SortDirection.NONE, filters: [ { - name: ContainerRequestState.COMMITTED, + name: ProcessState.COMMITTED, selected: true, - type: ContainerRequestState.COMMITTED + type: ProcessState.COMMITTED }, { - name: ContainerRequestState.FINAL, + name: ProcessState.FINAL, selected: true, - type: ContainerRequestState.FINAL + type: ProcessState.FINAL }, { - name: ContainerRequestState.UNCOMMITTED, + name: ProcessState.UNCOMMITTED, selected: true, - type: ContainerRequestState.UNCOMMITTED + type: ProcessState.UNCOMMITTED } ], render: renderStatus, diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index 0f958d2cfb..f63584b786 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -11,7 +11,7 @@ import { DataColumns } from '~/components/data-table/data-table'; import { RouteComponentProps } from 'react-router'; import { RootState } from '~/store/store'; import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters'; -import { ContainerRequestState } from '~/models/container-request'; +import { ProcessState } from '~/models/process'; import { SortDirection } from '~/components/data-table/data-column'; import { ResourceKind } from '~/models/resource'; import { resourceLabel } from '~/common/labels'; @@ -47,7 +47,7 @@ export enum ProjectPanelColumnNames { } export interface ProjectPanelFilter extends DataTableFilterItem { - type: ResourceKind | ContainerRequestState; + type: ResourceKind | ProcessState; } export const columns: DataColumns = [ @@ -67,19 +67,19 @@ export const columns: DataColumns = [ sortDirection: SortDirection.NONE, filters: [ { - name: ContainerRequestState.COMMITTED, + name: ProcessState.COMMITTED, selected: true, - type: ContainerRequestState.COMMITTED + type: ProcessState.COMMITTED }, { - name: ContainerRequestState.FINAL, + name: ProcessState.FINAL, selected: true, - type: ContainerRequestState.FINAL + type: ProcessState.FINAL }, { - name: ContainerRequestState.UNCOMMITTED, + name: ProcessState.UNCOMMITTED, selected: true, - type: ContainerRequestState.UNCOMMITTED + type: ProcessState.UNCOMMITTED } ], render: renderStatus, diff --git a/src/views/trash-panel/trash-panel-item.ts b/src/views/trash-panel/trash-panel-item.ts new file mode 100644 index 0000000000..89164581d2 --- /dev/null +++ b/src/views/trash-panel/trash-panel-item.ts @@ -0,0 +1,27 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { GroupContentsResource } from "~/services/groups-service/groups-service"; +import { TrashResource } from "~/models/resource"; + +export interface TrashPanelItem { + uuid: string; + name: string; + kind: string; + fileSize?: number; + trashAt?: string; + deleteAt?: string; + isTrashed?: boolean; +} + +export function resourceToDataItem(r: GroupContentsResource): TrashPanelItem { + return { + uuid: r.uuid, + name: r.name, + kind: r.kind, + trashAt: (r as TrashResource).trashAt, + deleteAt: (r as TrashResource).deleteAt, + isTrashed: (r as TrashResource).isTrashed + }; +} diff --git a/src/views/trash-panel/trash-panel.tsx b/src/views/trash-panel/trash-panel.tsx new file mode 100644 index 0000000000..c5a302efb3 --- /dev/null +++ b/src/views/trash-panel/trash-panel.tsx @@ -0,0 +1,149 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { TrashPanelItem } from './trash-panel-item'; +import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; +import { DataExplorer } from "~/views-components/data-explorer/data-explorer"; +import { DispatchProp, connect } from 'react-redux'; +import { DataColumns } from '~/components/data-table/data-table'; +import { RouteComponentProps } from 'react-router'; +import { RootState } from '~/store/store'; +import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters'; +import { ProcessState } from '~/models/process'; +import { SortDirection } from '~/components/data-table/data-column'; +import { ResourceKind } from '~/models/resource'; +import { resourceLabel } from '~/common/labels'; +import { ArvadosTheme } from '~/common/custom-theme'; +import { renderName, renderType, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers'; +import { TrashIcon } from '~/components/icon/icon'; +import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-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 TrashPanelColumnNames { + NAME = "Name", + TYPE = "Type", + FILE_SIZE = "File size", + TRASHED_DATE = "Trashed date", + TO_BE_DELETED = "To be deleted" +} + +export interface TrashPanelFilter extends DataTableFilterItem { + type: ResourceKind | ProcessState; +} + +export const columns: DataColumns = [ + { + name: TrashPanelColumnNames.NAME, + selected: true, + configurable: true, + sortDirection: SortDirection.ASC, + filters: [], + render: renderName, + width: "450px" + }, + { + name: TrashPanelColumnNames.TYPE, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: [ + { + name: resourceLabel(ResourceKind.COLLECTION), + selected: true, + type: ResourceKind.COLLECTION + }, + { + name: resourceLabel(ResourceKind.PROCESS), + selected: true, + type: ResourceKind.PROCESS + }, + { + name: resourceLabel(ResourceKind.PROJECT), + selected: true, + type: ResourceKind.PROJECT + } + ], + render: item => renderType(item.kind), + width: "125px" + }, + { + name: TrashPanelColumnNames.FILE_SIZE, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: [], + render: item => renderFileSize(item.fileSize), + width: "50px" + }, + { + name: TrashPanelColumnNames.TRASHED_DATE, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: [], + render: item => renderDate(item.trashAt), + width: "50px" + }, + { + name: TrashPanelColumnNames.TO_BE_DELETED, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: [], + render: item => renderDate(item.deleteAt), + width: "50px" + }, +]; + +interface TrashPanelDataProps { + currentItemId: string; +} + +interface TrashPanelActionProps { + onItemClick: (item: TrashPanelItem) => void; + onContextMenu: (event: React.MouseEvent, item: TrashPanelItem) => void; + onDialogOpen: (ownerUuid: string) => void; + onItemDoubleClick: (item: TrashPanelItem) => void; + onItemRouteChange: (itemId: string) => void; +} + +type TrashPanelProps = TrashPanelDataProps & TrashPanelActionProps & DispatchProp + & WithStyles & RouteComponentProps<{ id: string }>; + +export const TrashPanel = withStyles(styles)( + connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))( + class extends React.Component { + render() { + return item.uuid} + defaultIcon={TrashIcon} + defaultMessages={['Your trash list is empty.']}/> + ; + } + + componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: TrashPanelProps) { + if (match.params.id !== currentItemId) { + onItemRouteChange(match.params.id); + } + } + } + ) +); diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index a2d61d5cd1..a3f7624f9f 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -49,6 +49,8 @@ import { MultipleFilesRemoveDialog } from '~/views-components/file-remove-dialog import { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected'; import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create'; import { PROJECT_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-project-create'; +import { TrashPanel } from "~/views/trash-panel/trash-panel"; +import { trashPanelActions } from "~/store/trash-panel/trash-panel-action"; const DRAWER_WITDH = 240; const APP_BAR_HEIGHT = 100; @@ -232,6 +234,7 @@ export const Workbench = withStyles(styles)( } /> + @@ -335,6 +338,33 @@ export const Workbench = withStyles(styles)( }} {...props} /> + renderTrashPanel = (props: RouteComponentProps<{ id: string }>) => this.props.dispatch(trashPanelActions.REQUEST_ITEMS())} + onContextMenu={(event, item) => { + const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE; + this.openContextMenu(event, { + uuid: item.uuid, + name: item.name, + kind, + }); + }} + onDialogOpen={this.handleProjectCreationDialogOpen} + onItemClick={item => { + this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind)); + }} + onItemDoubleClick={item => { + switch (item.kind) { + case ResourceKind.COLLECTION: + this.props.dispatch(loadCollection(item.uuid)); + this.props.dispatch(push(getCollectionUrl(item.uuid))); + default: + this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT)); + this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE)); + } + + }} + {...props} /> + mainAppBarActions: MainAppBarActionProps = { onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => { this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH)); -- 2.30.2