Merge branch '13694-Data-operations-Project-creation' of git.curoverse.com:arvados...
[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, withRouter } from "react-router";
10 import authActions from "../../store/auth/auth-action";
11 import dataExplorerActions from "../../store/data-explorer/data-explorer-action";
12 import { User } from "../../models/user";
13 import { RootState } from "../../store/store";
14 import MainAppBar, {
15     MainAppBarActionProps,
16     MainAppBarMenuItem
17 } from '../../views-components/main-app-bar/main-app-bar';
18 import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
19 import { push } from 'react-router-redux';
20 import ProjectTree from '../../views-components/project-tree/project-tree';
21 import { TreeItem } from "../../components/tree/tree";
22 import { Project } from "../../models/project";
23 import { getTreePath, findTreeItem } from '../../store/project/project-reducer';
24 import sidePanelActions from '../../store/side-panel/side-panel-action';
25 import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
26 import { ResourceKind } from "../../models/resource";
27 import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
28 import projectActions from "../../store/project/project-action";
29 import ProjectPanel from "../project-panel/project-panel";
30 import { sidePanelData } from '../../store/side-panel/side-panel-reducer';
31 import DetailsPanel from '../../views-components/details-panel/details-panel';
32 import { ArvadosTheme } from '../../common/custom-theme';
33 import ContextMenu from '../../components/context-menu/context-menu';
34 import { mockAnchorFromMouseEvent } from '../../components/popover/helpers';
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     };
101     isCreationDialogOpen: boolean;
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         },
117         isCreationDialogOpen: false,
118         anchorEl: null,
119         searchText: "",
120         breadcrumbs: [],
121         menuItems: {
122             accountMenu: [
123                 {
124                     label: "Logout",
125                     action: () => this.props.dispatch(authActions.LOGOUT())
126                 },
127                 {
128                     label: "My account",
129                     action: () => this.props.dispatch(push("/my-account"))
130                 }
131             ],
132             helpMenu: [
133                 {
134                     label: "Help",
135                     action: () => this.props.dispatch(push("/help"))
136                 }
137             ],
138             anonymousMenu: [
139                 {
140                     label: "Sign in",
141                     action: () => this.props.dispatch(authActions.LOGIN())
142                 }
143             ]
144         },
145         isDetailsPanelOpened: false
146     };
147
148     mainAppBarActions: MainAppBarActionProps = {
149         onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
150             this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
151         },
152         onSearch: searchText => {
153             this.setState({ searchText });
154             this.props.dispatch(push(`/search?q=${searchText}`));
155         },
156         onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
157         onDetailsPanelToggle: () => {
158             this.setState(prev => ({ isDetailsPanelOpened: !prev.isDetailsPanelOpened }));
159         },
160         onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => {
161             this.openContextMenu(event, breadcrumb);
162         }
163     };
164
165     toggleSidePanelOpen = (itemId: string) => {
166         this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
167     }
168
169     toggleSidePanelActive = (itemId: string) => {
170         this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
171         this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
172     }
173
174     handleCreationDialogOpen = () => {
175         this.setState({ isCreationDialogOpen: true });
176     }
177
178     handleCreationDialogClose = () => {
179         this.setState({ isCreationDialogOpen: false });
180     }
181
182     openContextMenu = (event: React.MouseEvent<HTMLElement>, item: any) => {
183         event.preventDefault();
184         this.setState({ contextMenu: { anchorEl: mockAnchorFromMouseEvent(event) } });
185         console.log(item);
186     }
187
188     closeContextMenu = () => {
189         this.setState({ contextMenu: {} });
190     }
191
192     render() {
193         const path = getTreePath(this.props.projects, this.props.currentProjectId);
194         const breadcrumbs = path.map(item => ({
195             label: item.data.name,
196             itemId: item.data.uuid,
197             status: item.status
198         }));
199
200         const { classes, user } = this.props;
201         return (
202             <div className={classes.root}>
203                 <div className={classes.appBar}>
204                     <MainAppBar
205                         breadcrumbs={breadcrumbs}
206                         searchText={this.state.searchText}
207                         user={this.props.user}
208                         menuItems={this.state.menuItems}
209                         {...this.mainAppBarActions}
210                     />
211                 </div>
212                 {user &&
213                     <Drawer
214                         variant="permanent"
215                         classes={{
216                             paper: classes.drawerPaper,
217                         }}>
218                         <div className={classes.toolbar} />
219                         <SidePanel
220                             toggleOpen={this.toggleSidePanelOpen}
221                             toggleActive={this.toggleSidePanelActive}
222                             sidePanelItems={this.props.sidePanelItems}
223                             onContextMenu={this.openContextMenu}>
224                             <ProjectTree
225                                 projects={this.props.projects}
226                                 toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
227                                 toggleActive={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
228                                 onContextMenu={this.openContextMenu} />
229                         </SidePanel>
230                     </Drawer>}
231                 <main className={classes.contentWrapper}>
232                     <div className={classes.content}>
233                         <Switch>
234                             <Route path="/projects/:id" render={this.renderProjectPanel} />
235                         </Switch>
236                     </div>
237                     <DetailsPanel
238                         isOpened={this.state.isDetailsPanelOpened}
239                         onCloseDrawer={this.mainAppBarActions.onDetailsPanelToggle} />
240                 </main>
241                 <ContextMenu
242                     anchorEl={this.state.contextMenu.anchorEl}
243                     actions={contextMenuActions}
244                     onActionClick={console.log}
245                     onClose={this.closeContextMenu} />
246             </div>
247         );
248     }
249
250     renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
251         onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
252         onItemClick={item => this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE))}
253         onContextMenu={this.openContextMenu}
254         handleCreationDialogOpen={this.handleCreationDialogOpen}
255         handleCreationDialogClose={this.handleCreationDialogClose}
256         isCreationDialogOpen={this.state.isCreationDialogOpen}
257         {...props} />
258
259 }
260
261 const contextMenuActions = [[{
262     icon: "fas fa-plus fa-fw",
263     name: "New project"
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 );