21067: Fix bug deleting projects from shared-with-me
[arvados-workbench2.git] / src / views / project-panel / project-panel.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import withStyles from '@material-ui/core/styles/withStyles';
7 import { DispatchProp, connect } from 'react-redux';
8 import { RouteComponentProps } from 'react-router';
9 import { StyleRulesCallback, WithStyles } from '@material-ui/core';
10
11 import { DataExplorer } from 'views-components/data-explorer/data-explorer';
12 import { DataColumns } from 'components/data-table/data-table';
13 import { RootState } from 'store/store';
14 import { DataTableFilterItem } from 'components/data-table-filters/data-table-filters';
15 import { ContainerRequestState } from 'models/container-request';
16 import { SortDirection } from 'components/data-table/data-column';
17 import { ResourceKind, Resource } from 'models/resource';
18 import {
19     ResourceName,
20     ProcessStatus as ResourceStatus,
21     ResourceType,
22     ResourceOwnerWithName,
23     ResourcePortableDataHash,
24     ResourceFileSize,
25     ResourceFileCount,
26     ResourceUUID,
27     ResourceContainerUuid,
28     ContainerRunTime,
29     ResourceOutputUuid,
30     ResourceLogUuid,
31     ResourceParentProcess,
32     ResourceModifiedByUserUuid,
33     ResourceVersion,
34     ResourceCreatedAtDate,
35     ResourceLastModifiedDate,
36     ResourceTrashDate,
37     ResourceDeleteDate,
38 } from 'views-components/data-explorer/renderers';
39 import { ProjectIcon } from 'components/icon/icon';
40 import { ResourcesState, getResource } from 'store/resources/resources';
41 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
42 import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
43 import { navigateTo } from 'store/navigation/navigation-action';
44 import { getProperty } from 'store/properties/properties';
45 import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action';
46 import { ArvadosTheme } from 'common/custom-theme';
47 import { createTree } from 'models/tree';
48 import { getInitialResourceTypeFilters, getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
49 import { GroupContentsResource } from 'services/groups-service/groups-service';
50 import { GroupClass, GroupResource } from 'models/group';
51 import { CollectionResource } from 'models/collection';
52 import { resourceIsFrozen } from 'common/frozen-resources';
53 import { ProjectResource } from 'models/project';
54 import { NotFoundView } from 'views/not-found-panel/not-found-panel';
55
56 type CssRules = 'root' | 'button';
57
58 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
59     root: {
60         width: '100%',
61     },
62     button: {
63         marginLeft: theme.spacing.unit,
64     },
65 });
66
67 export enum ProjectPanelColumnNames {
68     NAME = 'Name',
69     STATUS = 'Status',
70     TYPE = 'Type',
71     OWNER = 'Owner',
72     PORTABLE_DATA_HASH = 'Portable Data Hash',
73     FILE_SIZE = 'File Size',
74     FILE_COUNT = 'File Count',
75     UUID = 'UUID',
76     CONTAINER_UUID = 'Container UUID',
77     RUNTIME = 'Runtime',
78     OUTPUT_UUID = 'Output UUID',
79     LOG_UUID = 'Log UUID',
80     PARENT_PROCESS = 'Parent Process UUID',
81     MODIFIED_BY_USER_UUID = 'Modified by User UUID',
82     VERSION = 'Version',
83     CREATED_AT = 'Date Created',
84     LAST_MODIFIED = 'Last Modified',
85     TRASH_AT = 'Trash at',
86     DELETE_AT = 'Delete at',
87 }
88
89 export interface ProjectPanelFilter extends DataTableFilterItem {
90     type: ResourceKind | ContainerRequestState;
91 }
92
93 export const projectPanelColumns: DataColumns<string, ProjectResource> = [
94     {
95         name: ProjectPanelColumnNames.NAME,
96         selected: true,
97         configurable: true,
98         sort: { direction: SortDirection.NONE, field: 'name' },
99         filters: createTree(),
100         render: (uuid) => <ResourceName uuid={uuid} />,
101     },
102     {
103         name: ProjectPanelColumnNames.STATUS,
104         selected: true,
105         configurable: true,
106         mutuallyExclusiveFilters: true,
107         filters: getInitialProcessStatusFilters(),
108         render: (uuid) => <ResourceStatus uuid={uuid} />,
109     },
110     {
111         name: ProjectPanelColumnNames.TYPE,
112         selected: true,
113         configurable: true,
114         filters: getInitialResourceTypeFilters(),
115         render: (uuid) => <ResourceType uuid={uuid} />,
116     },
117     {
118         name: ProjectPanelColumnNames.OWNER,
119         selected: false,
120         configurable: true,
121         filters: createTree(),
122         render: (uuid) => <ResourceOwnerWithName uuid={uuid} />,
123     },
124     {
125         name: ProjectPanelColumnNames.PORTABLE_DATA_HASH,
126         selected: false,
127         configurable: true,
128         filters: createTree(),
129         render: (uuid) => <ResourcePortableDataHash uuid={uuid} />,
130     },
131     {
132         name: ProjectPanelColumnNames.FILE_SIZE,
133         selected: true,
134         configurable: true,
135         filters: createTree(),
136         render: (uuid) => <ResourceFileSize uuid={uuid} />,
137     },
138     {
139         name: ProjectPanelColumnNames.FILE_COUNT,
140         selected: false,
141         configurable: true,
142         filters: createTree(),
143         render: (uuid) => <ResourceFileCount uuid={uuid} />,
144     },
145     {
146         name: ProjectPanelColumnNames.UUID,
147         selected: false,
148         configurable: true,
149         filters: createTree(),
150         render: (uuid) => <ResourceUUID uuid={uuid} />,
151     },
152     {
153         name: ProjectPanelColumnNames.CONTAINER_UUID,
154         selected: false,
155         configurable: true,
156         filters: createTree(),
157         render: (uuid) => <ResourceContainerUuid uuid={uuid} />,
158     },
159     {
160         name: ProjectPanelColumnNames.RUNTIME,
161         selected: false,
162         configurable: true,
163         filters: createTree(),
164         render: (uuid) => <ContainerRunTime uuid={uuid} />,
165     },
166     {
167         name: ProjectPanelColumnNames.OUTPUT_UUID,
168         selected: false,
169         configurable: true,
170         filters: createTree(),
171         render: (uuid) => <ResourceOutputUuid uuid={uuid} />,
172     },
173     {
174         name: ProjectPanelColumnNames.LOG_UUID,
175         selected: false,
176         configurable: true,
177         filters: createTree(),
178         render: (uuid) => <ResourceLogUuid uuid={uuid} />,
179     },
180     {
181         name: ProjectPanelColumnNames.PARENT_PROCESS,
182         selected: false,
183         configurable: true,
184         filters: createTree(),
185         render: (uuid) => <ResourceParentProcess uuid={uuid} />,
186     },
187     {
188         name: ProjectPanelColumnNames.MODIFIED_BY_USER_UUID,
189         selected: false,
190         configurable: true,
191         filters: createTree(),
192         render: (uuid) => <ResourceModifiedByUserUuid uuid={uuid} />,
193     },
194     {
195         name: ProjectPanelColumnNames.VERSION,
196         selected: false,
197         configurable: true,
198         filters: createTree(),
199         render: (uuid) => <ResourceVersion uuid={uuid} />,
200     },
201     {
202         name: ProjectPanelColumnNames.CREATED_AT,
203         selected: false,
204         configurable: true,
205         sort: { direction: SortDirection.NONE, field: 'createdAt' },
206         filters: createTree(),
207         render: (uuid) => <ResourceCreatedAtDate uuid={uuid} />,
208     },
209     {
210         name: ProjectPanelColumnNames.LAST_MODIFIED,
211         selected: true,
212         configurable: true,
213         sort: { direction: SortDirection.DESC, field: 'modifiedAt' },
214         filters: createTree(),
215         render: (uuid) => <ResourceLastModifiedDate uuid={uuid} />,
216     },
217     {
218         name: ProjectPanelColumnNames.TRASH_AT,
219         selected: false,
220         configurable: true,
221         sort: { direction: SortDirection.NONE, field: 'trashAt' },
222         filters: createTree(),
223         render: (uuid) => <ResourceTrashDate uuid={uuid} />,
224     },
225     {
226         name: ProjectPanelColumnNames.DELETE_AT,
227         selected: false,
228         configurable: true,
229         sort: { direction: SortDirection.NONE, field: 'deleteAt' },
230         filters: createTree(),
231         render: (uuid) => <ResourceDeleteDate uuid={uuid} />,
232     },
233 ];
234
235 export const PROJECT_PANEL_ID = 'projectPanel';
236
237 const DEFAULT_VIEW_MESSAGES = ['Your project is empty.', 'Please create a project or create a collection and upload a data.'];
238
239 interface ProjectPanelDataProps {
240     currentItemId: string;
241     resources: ResourcesState;
242     project: GroupResource;
243     isAdmin: boolean;
244     userUuid: string;
245     dataExplorerItems: any;
246 }
247
248 type ProjectPanelProps = ProjectPanelDataProps & DispatchProp & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
249
250 const mapStateToProps = (state: RootState) => {
251     const currentItemId = getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
252     const project = getResource<GroupResource>(currentItemId || "")(state.resources);
253     return {
254         currentItemId,
255         project,
256         resources: state.resources,
257         userUuid: state.auth.user!.uuid,
258     };
259 }
260
261 export const ProjectPanel = withStyles(styles)(
262     connect(mapStateToProps)(
263         class extends React.Component<ProjectPanelProps> {
264             render() {
265                 const { classes } = this.props;
266
267                 return this.props.project ?
268                     <div data-cy='project-panel' className={classes.root}>
269                         <DataExplorer
270                             id={PROJECT_PANEL_ID}
271                             onRowClick={this.handleRowClick}
272                             onRowDoubleClick={this.handleRowDoubleClick}
273                             onContextMenu={this.handleContextMenu}
274                             contextMenuColumn={true}
275                             defaultViewIcon={ProjectIcon}
276                             defaultViewMessages={DEFAULT_VIEW_MESSAGES}
277                         />
278                     </div>
279                     :
280                     <NotFoundView
281                         icon={ProjectIcon}
282                         messages={["Project not found"]}
283                     />
284             }
285
286             isCurrentItemChild = (resource: Resource) => {
287                 return resource.ownerUuid === this.props.currentItemId;
288             };
289
290             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
291                 const { resources, isAdmin } = this.props;
292                 const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
293                 // When viewing the contents of a filter group, all contents should be treated as read only.
294                 let readonly = false;
295                 const project = getResource<GroupResource>(this.props.currentItemId)(resources);
296                 if (project && project.groupClass === GroupClass.FILTER) {
297                     readonly = true;
298                 }
299
300                 const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid, readonly));
301                 if (menuKind && resource) {
302                     this.props.dispatch<any>(
303                         openContextMenu(event, {
304                             name: resource.name,
305                             uuid: resource.uuid,
306                             ownerUuid: resource.ownerUuid,
307                             isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
308                             kind: resource.kind,
309                             menuKind,
310                             isAdmin,
311                             isFrozen: resourceIsFrozen(resource, resources),
312                             description: resource.description,
313                             storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
314                             properties: 'properties' in resource ? resource.properties : {},
315                         })
316                     );
317                 }
318                 this.props.dispatch<any>(loadDetailsPanel(resourceUuid));
319             };
320
321             handleRowDoubleClick = (uuid: string) => {
322                 this.props.dispatch<any>(navigateTo(uuid));
323             };
324
325             handleRowClick = (uuid: string) => {
326                 this.props.dispatch<any>(loadDetailsPanel(uuid));
327             };
328         }
329     )
330 );