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