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