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