Merge branch '13797-refatoring-part2'
[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, MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar';
14 import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
15 import { push } from 'react-router-redux';
16 import { ProjectTree } from '../../views-components/project-tree/project-tree';
17 import { TreeItem } from "../../components/tree/tree";
18 import { getTreePath } from '../../store/project/project-reducer';
19 import { sidePanelActions } from '../../store/side-panel/side-panel-action';
20 import { SidePanel, SidePanelItem } from '../../components/side-panel/side-panel';
21 import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
22 import { projectActions } from "../../store/project/project-action";
23 import { ProjectPanel } from "../project-panel/project-panel";
24 import { DetailsPanel } from '../../views-components/details-panel/details-panel';
25 import { ArvadosTheme } from '../../common/custom-theme';
26 import { CreateProjectDialog } from "../../views-components/create-project-dialog/create-project-dialog";
27 import { authService } from '../../services/services';
28
29 import { detailsPanelActions, loadDetails } from "../../store/details-panel/details-panel-action";
30 import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
31 import { SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer';
32 import { ProjectResource } from '../../models/project';
33 import { ResourceKind } from '../../models/resource';
34 import { ContextMenu, ContextMenuKind } from "../../views-components/context-menu/context-menu";
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<ProjectResource>>;
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     anchorEl: any;
99     searchText: string;
100     menuItems: {
101         accountMenu: NavMenuItem[],
102         helpMenu: NavMenuItem[],
103         anonymousMenu: NavMenuItem[]
104     };
105 }
106
107 export const Workbench = withStyles(styles)(
108     connect<WorkbenchDataProps>(
109         (state: RootState) => ({
110             projects: state.projects.items,
111             currentProjectId: state.projects.currentItemId,
112             user: state.auth.user,
113             sidePanelItems: state.sidePanel
114         })
115     )(
116         class extends React.Component<WorkbenchProps, WorkbenchState> {
117             state = {
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             };
147
148             render() {
149                 const path = getTreePath(this.props.projects, this.props.currentProjectId);
150                 const breadcrumbs = path.map(item => ({
151                     label: item.data.name,
152                     itemId: item.data.uuid,
153                     status: item.status
154                 }));
155
156                 const { classes, user } = this.props;
157                 return (
158                     <div className={classes.root}>
159                         <div className={classes.appBar}>
160                             <MainAppBar
161                                 breadcrumbs={breadcrumbs}
162                                 searchText={this.state.searchText}
163                                 user={this.props.user}
164                                 menuItems={this.state.menuItems}
165                                 {...this.mainAppBarActions} />
166                         </div>
167                         {user &&
168                             <Drawer
169                                 variant="permanent"
170                                 classes={{
171                                     paper: classes.drawerPaper,
172                                 }}>
173                                 <div className={classes.toolbar} />
174                                 <SidePanel
175                                     toggleOpen={this.toggleSidePanelOpen}
176                                     toggleActive={this.toggleSidePanelActive}
177                                     sidePanelItems={this.props.sidePanelItems}
178                                     onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "", ContextMenuKind.RootProject)}>
179                                     <ProjectTree
180                                         projects={this.props.projects}
181                                         toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
182                                         onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid, ContextMenuKind.Project)}
183                                         toggleActive={itemId => {
184                                             this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
185                                             this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
186                                             this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.Projects));
187                                         }} />
188                                 </SidePanel>
189                             </Drawer>}
190                         <main className={classes.contentWrapper}>
191                             <div className={classes.content}>
192                                 <Switch>
193                                     <Route path="/projects/:id" render={this.renderProjectPanel} />
194                                 </Switch>
195                             </div>
196                             { user && <DetailsPanel /> }
197                         </main>
198                         <ContextMenu />
199                         <CreateProjectDialog />
200                     </div>
201                 );
202             }
203
204             renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
205                 onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
206                 onContextMenu={(event, item) => this.openContextMenu(event, item.uuid, ContextMenuKind.Project)}
207                 onDialogOpen={this.handleCreationDialogOpen}
208                 onItemClick={item => {
209                     this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
210                 }}
211                 onItemDoubleClick={item => {
212                     this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
213                     this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.Project));
214                 }}
215                 {...props} />
216
217             mainAppBarActions: MainAppBarActionProps = {
218                 onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
219                     this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
220                     this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
221                 },
222                 onSearch: searchText => {
223                     this.setState({ searchText });
224                     this.props.dispatch(push(`/search?q=${searchText}`));
225                 },
226                 onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
227                 onDetailsPanelToggle: () => {
228                     this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
229                 },
230                 onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
231                     this.openContextMenu(event, breadcrumb.itemId, ContextMenuKind.Project);
232                 }
233             };
234
235             toggleSidePanelOpen = (itemId: string) => {
236                 this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
237             }
238
239             toggleSidePanelActive = (itemId: string) => {
240                 this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
241                 this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
242                 this.props.dispatch(push("/"));
243             }
244
245             handleCreationDialogOpen = (itemUuid: string) => {
246                 this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
247             }
248
249             openContextMenu = (event: React.MouseEvent<HTMLElement>, itemUuid: string, kind: ContextMenuKind) => {
250                 event.preventDefault();
251                 this.props.dispatch(
252                     contextMenuActions.OPEN_CONTEXT_MENU({
253                         position: { x: event.clientX, y: event.clientY },
254                         resource: { uuid: itemUuid, kind }
255                     })
256                 );
257             }
258         }
259     )
260 );