Extract major components from workbench
[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 * as React from 'react';
6 import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
7 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
8 import { DispatchProp, connect } from 'react-redux';
9 import { DataColumns } from '~/components/data-table/data-table';
10 import { RouteComponentProps } from 'react-router';
11 import { RootState } from '~/store/store';
12 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
13 import { ContainerRequestState } from '~/models/container-request';
14 import { SortDirection } from '~/components/data-table/data-column';
15 import { ResourceKind } from '~/models/resource';
16 import { resourceLabel } from '~/common/labels';
17 import { ArvadosTheme } from '~/common/custom-theme';
18 import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
19 import { restoreBranch, setProjectItem, ItemMode } from '~/store/navigation/navigation-action';
20 import { ProjectIcon } from '~/components/icon/icon';
21 import { ResourceName } from '~/views-components/data-explorer/renderers';
22 import { ResourcesState, getResource } from '~/store/resources/resources';
23 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
24 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
25 import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
26 import { CollectionResource } from '~/models/collection';
27 import { ProjectResource } from '~/models/project';
28 import { openProjectCreator } from '~/store/project/project-action';
29 import { reset } from 'redux-form';
30 import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
31 import { collectionCreateActions } from '~/store/collections/creator/collection-creator-action';
32 import { navigateToResource } from '~/store/navigation/navigation-action';
33
34 type CssRules = 'root' | "toolbar" | "button";
35
36 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
37     root: {
38         position: 'relative',
39         width: '100%',
40         height: '100%'
41     },
42     toolbar: {
43         paddingBottom: theme.spacing.unit * 3,
44         textAlign: "right"
45     },
46     button: {
47         marginLeft: theme.spacing.unit
48     },
49 });
50
51 export enum ProjectPanelColumnNames {
52     NAME = "Name",
53     STATUS = "Status",
54     TYPE = "Type",
55     OWNER = "Owner",
56     FILE_SIZE = "File size",
57     LAST_MODIFIED = "Last modified"
58 }
59
60 export interface ProjectPanelFilter extends DataTableFilterItem {
61     type: ResourceKind | ContainerRequestState;
62 }
63
64 export const columns: DataColumns<string, ProjectPanelFilter> = [
65     {
66         name: ProjectPanelColumnNames.NAME,
67         selected: true,
68         configurable: true,
69         sortDirection: SortDirection.ASC,
70         filters: [],
71         render: uuid => <ResourceName uuid={uuid} />,
72         width: "450px"
73     },
74     {
75         name: "Status",
76         selected: true,
77         configurable: true,
78         sortDirection: SortDirection.NONE,
79         filters: [
80             {
81                 name: ContainerRequestState.COMMITTED,
82                 selected: true,
83                 type: ContainerRequestState.COMMITTED
84             },
85             {
86                 name: ContainerRequestState.FINAL,
87                 selected: true,
88                 type: ContainerRequestState.FINAL
89             },
90             {
91                 name: ContainerRequestState.UNCOMMITTED,
92                 selected: true,
93                 type: ContainerRequestState.UNCOMMITTED
94             }
95         ],
96         render: uuid => <ProcessStatus uuid={uuid} />,
97         width: "75px"
98     },
99     {
100         name: ProjectPanelColumnNames.TYPE,
101         selected: true,
102         configurable: true,
103         sortDirection: SortDirection.NONE,
104         filters: [
105             {
106                 name: resourceLabel(ResourceKind.COLLECTION),
107                 selected: true,
108                 type: ResourceKind.COLLECTION
109             },
110             {
111                 name: resourceLabel(ResourceKind.PROCESS),
112                 selected: true,
113                 type: ResourceKind.PROCESS
114             },
115             {
116                 name: resourceLabel(ResourceKind.PROJECT),
117                 selected: true,
118                 type: ResourceKind.PROJECT
119             }
120         ],
121         render: uuid => <ResourceType uuid={uuid} />,
122         width: "125px"
123     },
124     {
125         name: ProjectPanelColumnNames.OWNER,
126         selected: true,
127         configurable: true,
128         sortDirection: SortDirection.NONE,
129         filters: [],
130         render: uuid => <ResourceOwner uuid={uuid} />,
131         width: "200px"
132     },
133     {
134         name: ProjectPanelColumnNames.FILE_SIZE,
135         selected: true,
136         configurable: true,
137         sortDirection: SortDirection.NONE,
138         filters: [],
139         render: uuid => <ResourceFileSize uuid={uuid} />,
140         width: "50px"
141     },
142     {
143         name: ProjectPanelColumnNames.LAST_MODIFIED,
144         selected: true,
145         configurable: true,
146         sortDirection: SortDirection.NONE,
147         filters: [],
148         render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
149         width: "150px"
150     }
151 ];
152
153 export const PROJECT_PANEL_ID = "projectPanel";
154
155 interface ProjectPanelDataProps {
156     currentItemId: string;
157     resources: ResourcesState;
158 }
159
160 type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
161     & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
162
163 export const ProjectPanel = withStyles(styles)(
164     connect((state: RootState) => ({ currentItemId: state.projects.currentItemId, resources: state.resources }))(
165         class extends React.Component<ProjectPanelProps> {
166             render() {
167                 const { classes } = this.props;
168                 return <div className={classes.root}>
169                     <div className={classes.toolbar}>
170                         <Button color="primary" onClick={this.handleNewCollectionClick} variant="raised" className={classes.button}>
171                             Create a collection
172                         </Button>
173                         <Button color="primary" variant="raised" className={classes.button}>
174                             Run a process
175                         </Button>
176                         <Button color="primary" onClick={this.handleNewProjectClick} variant="raised" className={classes.button}>
177                             New project
178                         </Button>
179                     </div>
180                     <DataExplorer
181                         id={PROJECT_PANEL_ID}
182                         columns={columns}
183                         onRowClick={this.handleRowClick}
184                         onRowDoubleClick={this.handleRowDoubleClick}
185                         onContextMenu={this.handleContextMenu}
186                         defaultIcon={ProjectIcon}
187                         defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
188                 </div>;
189             }
190
191             handleNewProjectClick = () => {
192                 this.props.dispatch<any>(openProjectCreator(this.props.currentItemId));
193             }
194
195             handleNewCollectionClick = () => {
196                 this.props.dispatch(reset(COLLECTION_CREATE_DIALOG));
197                 this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: this.props.currentItemId }));
198             }
199
200             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
201                 event.preventDefault();
202                 const resource = getResource(resourceUuid)(this.props.resources) as CollectionResource | ProjectResource | undefined;
203                 if (resource) {
204                     let kind: ContextMenuKind;
205
206                     if (resource.kind === ResourceKind.PROJECT) {
207                         kind = ContextMenuKind.PROJECT;
208                     } else if (resource.kind === ResourceKind.COLLECTION) {
209                         kind = ContextMenuKind.COLLECTION_RESOURCE;
210                     } else {
211                         kind = ContextMenuKind.RESOURCE;
212                     }
213                     if (kind !== ContextMenuKind.RESOURCE) {
214                         this.props.dispatch(
215                             contextMenuActions.OPEN_CONTEXT_MENU({
216                                 position: { x: event.clientX, y: event.clientY },
217                                 resource: {
218                                     uuid: resource.uuid,
219                                     name: resource.name || '',
220                                     description: resource.description,
221                                     kind,
222                                 }
223                             })
224                         );
225                     }
226                 }
227             }
228
229             handleRowDoubleClick = (uuid: string) => {
230                 this.props.dispatch<any>(navigateToResource(uuid));
231             }
232
233             handleRowClick = (uuid: string) => {
234                 this.props.dispatch(loadDetailsPanel(uuid));
235             }
236
237             async componentDidMount() {
238                 if (this.props.match.params.id && this.props.currentItemId === '') {
239                     await this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
240                     this.props.dispatch<any>(setProjectItem(this.props.match.params.id, ItemMode.BOTH));
241                 }
242             }
243         }
244     )
245 );