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