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