Add trash view
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 21 Aug 2018 08:17:44 +0000 (10:17 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 21 Aug 2018 08:17:44 +0000 (10:17 +0200)
Feature #13828

Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

23 files changed:
src/common/formatters.ts
src/components/data-table/data-table.tsx
src/models/collection.ts
src/models/container-request.ts [deleted file]
src/models/group.ts
src/models/process.ts
src/models/project.ts
src/models/resource.ts
src/models/test-utils.ts
src/services/groups-service/groups-service.ts
src/services/services.ts
src/services/trash-service/trash-service.test.ts [new file with mode: 0644]
src/services/trash-service/trash-service.ts [new file with mode: 0644]
src/store/side-panel/side-panel-reducer.ts
src/store/store.ts
src/store/trash-panel/trash-panel-action.ts [new file with mode: 0644]
src/store/trash-panel/trash-panel-middleware-service.ts [new file with mode: 0644]
src/views-components/data-explorer/renderers.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/trash-panel/trash-panel-item.ts [new file with mode: 0644]
src/views/trash-panel/trash-panel.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx

index 49e0690515e868a4b1145c79ccbd2ede9d813314..b1baee7de912e56a4b22b683cd172dfdea542a1c 100644 (file)
@@ -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) => {
index 34f8168a55b50fc206829114846b46d8a51ff323..5a6f9e5a5b5337abce7b07d738206cbd84d56c78 100644 (file)
@@ -63,7 +63,7 @@ export const DataTable = withStyles(styles)(
             return <TableCell key={key || index} style={{ width: column.width, minWidth: column.width }}>
                 {renderHeader ?
                     renderHeader() :
-                    filters
+                    filters.length > 0
                         ? <DataTableFilters
                             name={`${name} filters`}
                             onChange={filters =>
index 0e96f7fd3dd6f1473f62d8c2a2e3226aa42f6507..5215998956cde1d419b00e1c47a71beab565f4eb 100644 (file)
@@ -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 (file)
index d1bcc36..0000000
+++ /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;
-}
index 5e8d7a1e0b4a2e76ceaf9c80d035f55417aa55d6..5319250e1bfc406714e30a2f3a909b337488dc19 100644 (file)
@@ -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
+}
index 1e04cb10f395de5284765fbb50244d2e45d4ea1d..bcfbd3a557faa074bb9c654fa50eadd39039dfd7 100644 (file)
@@ -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;
+}
index b919450774f687084a700d8b4d041772aa142669..8e101ce29ffeaac99cb7c2073aae96b8b79ac9e8 100644 (file)
@@ -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;
index 6a76b070452146734b2a9f64a36bf3d4eac6ff4f..ab487da070c29c5a30632266477c38959d548012 100644 (file)
@@ -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",
index 6723430c34872848c26a6b9fa8be172b780f5603..49eea605d563d106cb53cc63817f3074227576f7 100644 (file)
@@ -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";
 
index 822c810ef7ed0203666bdba829c0b13fced7d5ba..39cc74a9c2406a64041999cf0dceb2071b291cf8 100644 (file)
@@ -16,6 +16,7 @@ export interface ContentsArguments {
     order?: string;
     filters?: string;
     recursive?: boolean;
+    includeTrash?: boolean;
 }
 
 export type GroupContentsResource =
index 61dd399206384c159d0ffe6982e704fe054a405a..91997552c0ee1998cb358567706af50020d83689 100644 (file)
@@ -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<typeof createServices>;
 
@@ -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 (file)
index 0000000..f22d066
--- /dev/null
@@ -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 (file)
index 0000000..fc02d2f
--- /dev/null
@@ -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);
+    }
+}
index db1cbe5de51a7133b0a26f00a4f427223872e5d9..b68ce7a1c20026df63768ab0e7bfdff2edd858ab 100644 (file)
@@ -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());
         }
     }
 ];
index a4bf9d6e3b9692d00e92bfcdc59ec93fa66c89bb..febaf933d0e099763be33a25cfc75299536c4ce5 100644 (file)
@@ -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 (file)
index 0000000..84d5602
--- /dev/null
@@ -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 (file)
index 0000000..2d1dbf7
--- /dev/null
@@ -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<Dispatch, RootState>) {
+        const dataExplorer = api.getState().dataExplorer[this.getId()];
+        const columns = dataExplorer.columns as DataColumns<TrashPanelItem, TrashPanelFilter>;
+        const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+        const typeFilters = this.getColumnFilters(columns, TrashPanelColumnNames.TYPE);
+
+        const order = new OrderBuilder<ProjectResource>();
+
+        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<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+            })
+            .catch(() => {
+                api.dispatch(trashPanelActions.SET_ITEMS({
+                    items: [],
+                    itemsAvailable: 0,
+                    page: 0,
+                    rowsPerPage: dataExplorer.rowsPerPage
+                }));
+            });
+    }
+}
index 1b07642ab727da755b898553ebc29a768f6bc025..8246d02c37d6cdf8338359537140f90050126256 100644 (file)
@@ -42,7 +42,7 @@ export const renderIcon = (item: {kind: string}) => {
     }
 };
 
-export const renderDate = (date: string) => {
+export const renderDate = (date?: string) => {
     return <Typography noWrap>{formatDate(date)}</Typography>;
 };
 
index 125ea27ddf1635836bdd592091025d637efd028a..49f1f4ab3a593bc96fb2e10bdddeca294dc49c03 100644 (file)
@@ -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<FavoritePanelItem, FavoritePanelFilter> = [
@@ -62,19 +62,19 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         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,
index 0f958d2cfbbb87684c47c1d77a16319f3e494e28..f63584b7866d40b9ce766f9eb01bb2530e55ae98 100644 (file)
@@ -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<ProjectPanelItem, ProjectPanelFilter> = [
@@ -67,19 +67,19 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         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 (file)
index 0000000..8916458
--- /dev/null
@@ -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 (file)
index 0000000..c5a302e
--- /dev/null
@@ -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<CssRules> = (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<TrashPanelItem, TrashPanelFilter> = [
+    {
+        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<HTMLElement>, item: TrashPanelItem) => void;
+    onDialogOpen: (ownerUuid: string) => void;
+    onItemDoubleClick: (item: TrashPanelItem) => void;
+    onItemRouteChange: (itemId: string) => void;
+}
+
+type TrashPanelProps = TrashPanelDataProps & TrashPanelActionProps & DispatchProp
+                        & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+
+export const TrashPanel = withStyles(styles)(
+    connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+        class extends React.Component<TrashPanelProps> {
+            render() {
+                return <DataExplorer
+                    id={TRASH_PANEL_ID}
+                    columns={columns}
+                    onRowClick={this.props.onItemClick}
+                    onRowDoubleClick={this.props.onItemDoubleClick}
+                    onContextMenu={this.props.onContextMenu}
+                    extractKey={(item: TrashPanelItem) => item.uuid}
+                    defaultIcon={TrashIcon}
+                    defaultMessages={['Your trash list is empty.']}/>
+                ;
+            }
+
+            componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: TrashPanelProps) {
+                if (match.params.id !== currentItemId) {
+                    onItemRouteChange(match.params.id);
+                }
+            }
+        }
+    )
+);
index a2d61d5cd17a209351fa5bf3f228479df5087092..a3f7624f9f3805c277d0b9bf7fc968d458e32660 100644 (file)
@@ -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)(
                                     <Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`}  />} />
                                     <Route path="/projects/:id" render={this.renderProjectPanel} />
                                     <Route path="/favorites" render={this.renderFavoritePanel} />
+                                    <Route path="/trash" render={this.renderTrashPanel} />
                                     <Route path="/collections/:id" render={this.renderCollectionPanel} />
                                 </Switch>
                             </div>
@@ -335,6 +338,33 @@ export const Workbench = withStyles(styles)(
                 }}
                 {...props} />
 
+            renderTrashPanel = (props: RouteComponentProps<{ id: string }>) => <TrashPanel
+                onItemRouteChange={() => 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));