Merge remote-tracking branch 'origin' into 13694-Data-operations-Project-creation
[arvados-workbench2.git] / src / views / workbench / workbench.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 { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
7 import Drawer from '@material-ui/core/Drawer';
8 import { connect, DispatchProp } from "react-redux";
9 import { Route, Switch, RouteComponentProps } from "react-router";
10 import authActions from "../../store/auth/auth-action";
11 import { User } from "../../models/user";
12 import { RootState } from "../../store/store";
13 import MainAppBar, {
14     MainAppBarActionProps,
15     MainAppBarMenuItem
16 } from '../../views-components/main-app-bar/main-app-bar';
17 import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
18 import { push } from 'react-router-redux';
19 import ProjectTree from '../../views-components/project-tree/project-tree';
20 import { TreeItem } from "../../components/tree/tree";
21 import { Project } from "../../models/project";
22 import { getTreePath } from '../../store/project/project-reducer';
23 import sidePanelActions from '../../store/side-panel/side-panel-action';
24 import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
25 import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
26 import projectActions from "../../store/project/project-action";
27 import ProjectPanel from "../project-panel/project-panel";
28 import DetailsPanel from '../../views-components/details-panel/details-panel';
29 import { ArvadosTheme } from '../../common/custom-theme';
30 import ContextMenu, { ContextMenuAction } from '../../components/context-menu/context-menu';
31 import { mockAnchorFromMouseEvent } from '../../components/popover/helpers';
32 import DialogProjectCreate from '../../components/dialog-create/dialog-project-create';
33
34 const drawerWidth = 240;
35 const appBarHeight = 100;
36
37 type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
38
39 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
40     root: {
41         flexGrow: 1,
42         zIndex: 1,
43         overflow: 'hidden',
44         position: 'relative',
45         display: 'flex',
46         width: '100vw',
47         height: '100vh'
48     },
49     appBar: {
50         zIndex: theme.zIndex.drawer + 1,
51         position: "absolute",
52         width: "100%"
53     },
54     drawerPaper: {
55         position: 'relative',
56         width: drawerWidth,
57         display: 'flex',
58         flexDirection: 'column',
59     },
60     contentWrapper: {
61         backgroundColor: theme.palette.background.default,
62         display: "flex",
63         flexGrow: 1,
64         minWidth: 0,
65         paddingTop: appBarHeight
66     },
67     content: {
68         padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`,
69         overflowY: "auto",
70         flexGrow: 1
71     },
72     toolbar: theme.mixins.toolbar
73 });
74
75 interface WorkbenchDataProps {
76     projects: Array<TreeItem<Project>>;
77     currentProjectId: string;
78     user?: User;
79     sidePanelItems: SidePanelItem[];
80 }
81
82 interface WorkbenchActionProps {
83 }
84
85 type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
86
87 interface NavBreadcrumb extends Breadcrumb {
88     itemId: string;
89 }
90
91 interface NavMenuItem extends MainAppBarMenuItem {
92     action: () => void;
93 }
94
95 interface WorkbenchState {
96     contextMenu: {
97         anchorEl?: HTMLElement;
98     };
99     isCreationDialogOpen: boolean;
100     anchorEl: any;
101     searchText: string;
102     menuItems: {
103         accountMenu: NavMenuItem[],
104         helpMenu: NavMenuItem[],
105         anonymousMenu: NavMenuItem[]
106     };
107     isDetailsPanelOpened: boolean;
108 }
109
110 class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
111     state = {
112         contextMenu: {
113             anchorEl: undefined
114         },
115         isCreationDialogOpen: false,
116         anchorEl: null,
117         searchText: "",
118         breadcrumbs: [],
119         menuItems: {
120             accountMenu: [
121                 {
122                     label: "Logout",
123                     action: () => this.props.dispatch(authActions.LOGOUT())
124                 },
125                 {
126                     label: "My account",
127                     action: () => this.props.dispatch(push("/my-account"))
128                 }
129             ],
130             helpMenu: [
131                 {
132                     label: "Help",
133                     action: () => this.props.dispatch(push("/help"))
134                 }
135             ],
136             anonymousMenu: [
137                 {
138                     label: "Sign in",
139                     action: () => this.props.dispatch(authActions.LOGIN())
140                 }
141             ]
142         },
143         isDetailsPanelOpened: false
144     };
145
146     mainAppBarActions: MainAppBarActionProps = {
147         onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
148             this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
149         },
150         onSearch: searchText => {
151             this.setState({ searchText });
152             this.props.dispatch(push(`/search?q=${searchText}`));
153         },
154         onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
155         onDetailsPanelToggle: () => {
156             this.setState(prev => ({ isDetailsPanelOpened: !prev.isDetailsPanelOpened }));
157         },
158         onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => {
159             this.openContextMenu(event, breadcrumb);
160         }
161     };
162
163     toggleSidePanelOpen = (itemId: string) => {
164         this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
165     }
166
167     toggleSidePanelActive = (itemId: string) => {
168         this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
169         this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
170     }
171
172     handleCreationDialogOpen = () => {
173         this.closeContextMenu();
174         this.setState({ isCreationDialogOpen: true });
175     }
176
177     handleCreationDialogClose = () => {
178         this.setState({ isCreationDialogOpen: false });
179     }
180
181     openContextMenu = (event: React.MouseEvent<HTMLElement>, item: any) => {
182         event.preventDefault();
183         this.setState({ contextMenu: { anchorEl: mockAnchorFromMouseEvent(event) } });
184         console.log(item);
185     }
186
187     closeContextMenu = () => {
188         this.setState({ contextMenu: {} });
189     }
190
191     openCreateDialog = (item: ContextMenuAction) =>
192         item.openCreateDialog ? this.handleCreationDialogOpen() : void 0
193
194     render() {
195         const path = getTreePath(this.props.projects, this.props.currentProjectId);
196         const breadcrumbs = path.map(item => ({
197             label: item.data.name,
198             itemId: item.data.uuid,
199             status: item.status
200         }));
201
202         const { classes, user } = this.props;
203         return (
204             <div className={classes.root}>
205                 <div className={classes.appBar}>
206                     <MainAppBar
207                         breadcrumbs={breadcrumbs}
208                         searchText={this.state.searchText}
209                         user={this.props.user}
210                         menuItems={this.state.menuItems}
211                         {...this.mainAppBarActions} />
212                 </div>
213                 {user &&
214                     <Drawer
215                         variant="permanent"
216                         classes={{
217                             paper: classes.drawerPaper,
218                         }}>
219                         <div className={classes.toolbar} />
220                         <SidePanel
221                             toggleOpen={this.toggleSidePanelOpen}
222                             toggleActive={this.toggleSidePanelActive}
223                             sidePanelItems={this.props.sidePanelItems}
224                             onContextMenu={this.openContextMenu}>
225                             <ProjectTree
226                                 projects={this.props.projects}
227                                 toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
228                                 toggleActive={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
229                                 onContextMenu={this.openContextMenu} />
230                         </SidePanel>
231                     </Drawer>}
232                 <main className={classes.contentWrapper}>
233                     <div className={classes.content}>
234                         <Switch>
235                             <Route path="/projects/:id" render={this.renderProjectPanel} />
236                         </Switch>
237                     </div>
238                     <DetailsPanel
239                         isOpened={this.state.isDetailsPanelOpened}
240                         onCloseDrawer={this.mainAppBarActions.onDetailsPanelToggle} />
241                 </main>
242                 <ContextMenu
243                     anchorEl={this.state.contextMenu.anchorEl}
244                     actions={contextMenuActions}
245                     onActionClick={this.openCreateDialog}
246                     onClose={this.closeContextMenu} />
247                 <DialogProjectCreate open={this.state.isCreationDialogOpen} handleClose={this.handleCreationDialogClose} />
248             </div>
249         );
250     }
251
252     renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
253         onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
254         onItemClick={item => this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE))}
255         onContextMenu={this.openContextMenu}
256         onDialogOpen={this.handleCreationDialogOpen}
257         {...props} />
258 }
259
260 const contextMenuActions = [[{
261     icon: "fas fa-plus fa-fw",
262     name: "New project",
263     openCreateDialog: true
264 }, {
265     icon: "fas fa-users fa-fw",
266     name: "Share"
267 }, {
268     icon: "fas fa-sign-out-alt fa-fw",
269     name: "Move to"
270 }, {
271     icon: "fas fa-star fa-fw",
272     name: "Add to favourite"
273 }, {
274     icon: "fas fa-edit fa-fw",
275     name: "Rename"
276 }, {
277     icon: "fas fa-copy fa-fw",
278     name: "Make a copy"
279 }, {
280     icon: "fas fa-download fa-fw",
281     name: "Download"
282 }], [{
283     icon: "fas fa-trash-alt fa-fw",
284     name: "Remove"
285 }
286 ]];
287
288 export default connect<WorkbenchDataProps>(
289     (state: RootState) => ({
290         projects: state.projects.items,
291         currentProjectId: state.projects.currentItemId,
292         user: state.auth.user,
293         sidePanelItems: state.sidePanel
294     })
295 )(
296     withStyles(styles)(Workbench)
297 );