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