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