Merge branch 'main' into 21067-process-panel-error
[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, Grid } 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 { DefaultView } from "components/default-view/default-view";
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                     : <Grid
280                         container
281                         alignItems="center"
282                         justify="center"
283                         style={{ minHeight: "100%" }}>
284                         <DefaultView
285                             icon={ProjectIcon}
286                             messages={["Project not found"]}
287                         />
288                     </Grid>;
289             }
290
291             isCurrentItemChild = (resource: Resource) => {
292                 return resource.ownerUuid === this.props.currentItemId;
293             };
294
295             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
296                 const { resources, isAdmin } = this.props;
297                 const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
298                 // When viewing the contents of a filter group, all contents should be treated as read only.
299                 let readonly = false;
300                 const project = getResource<GroupResource>(this.props.currentItemId)(resources);
301                 if (project && project.groupClass === GroupClass.FILTER) {
302                     readonly = true;
303                 }
304
305                 const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid, readonly));
306                 if (menuKind && resource) {
307                     this.props.dispatch<any>(
308                         openContextMenu(event, {
309                             name: resource.name,
310                             uuid: resource.uuid,
311                             ownerUuid: resource.ownerUuid,
312                             isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
313                             kind: resource.kind,
314                             menuKind,
315                             isAdmin,
316                             isFrozen: resourceIsFrozen(resource, resources),
317                             description: resource.description,
318                             storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
319                             properties: 'properties' in resource ? resource.properties : {},
320                         })
321                     );
322                 }
323                 this.props.dispatch<any>(loadDetailsPanel(resourceUuid));
324             };
325
326             handleRowDoubleClick = (uuid: string) => {
327                 this.props.dispatch<any>(navigateTo(uuid));
328             };
329
330             handleRowClick = (uuid: string) => {
331                 this.props.dispatch<any>(loadDetailsPanel(uuid));
332             };
333         }
334     )
335 );