Merge branch 'master'
[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, Theme, 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 } 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, { MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar';
15 import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
16 import { push } from 'react-router-redux';
17 import projectActions, { getProjectList } from "../../store/project/project-action";
18 import ProjectTree from '../../views-components/project-tree/project-tree';
19 import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
20 import { Project } from "../../models/project";
21 import { getTreePath, findTreeItem } from '../../store/project/project-reducer';
22 import ProjectPanel from '../project-panel/project-panel';
23 import { PROJECT_EXPLORER_ID } from '../../views-components/project-explorer/project-explorer';
24 import { ProjectExplorerItem } from '../../views-components/project-explorer/project-explorer-item';
25 import sidePanelActions from '../../store/side-panel/side-panel-action';
26 import { projectService } from '../../services/services';
27 import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
28
29 const drawerWidth = 240;
30 const appBarHeight = 102;
31
32 type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
33
34 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
35     root: {
36         flexGrow: 1,
37         zIndex: 1,
38         overflow: 'hidden',
39         position: 'relative',
40         display: 'flex',
41         width: '100vw',
42         height: '100vh'
43     },
44     appBar: {
45         zIndex: theme.zIndex.drawer + 1,
46         backgroundColor: '#692498',
47         position: "absolute",
48         width: "100%"
49     },
50     drawerPaper: {
51         position: 'relative',
52         width: drawerWidth,
53         display: 'flex',
54         flexDirection: 'column',
55     },
56     contentWrapper: {
57         backgroundColor: theme.palette.background.default,
58         display: "flex",
59         flexGrow: 1,
60         minWidth: 0,
61         paddingTop: appBarHeight
62     },
63     content: {
64         padding: theme.spacing.unit * 3,
65         overflowY: "auto",
66         flexGrow: 1
67     },
68     toolbar: theme.mixins.toolbar
69 });
70
71 interface WorkbenchDataProps {
72     projects: Array<TreeItem<Project>>;
73     user?: User;
74     sidePanelItems: SidePanelItem[];
75 }
76
77 interface WorkbenchActionProps {
78 }
79
80 type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
81
82 interface NavBreadcrumb extends Breadcrumb {
83     itemId: string;
84     status: TreeItemStatus;
85 }
86
87 interface NavMenuItem extends MainAppBarMenuItem {
88     action: () => void;
89 }
90
91 interface WorkbenchState {
92     anchorEl: any;
93     breadcrumbs: NavBreadcrumb[];
94     searchText: string;
95     menuItems: {
96         accountMenu: NavMenuItem[],
97         helpMenu: NavMenuItem[],
98         anonymousMenu: NavMenuItem[]
99     };
100 }
101
102 class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
103     state = {
104         anchorEl: null,
105         searchText: "",
106         breadcrumbs: [],
107         menuItems: {
108             accountMenu: [
109                 {
110                     label: "Logout",
111                     action: () => this.props.dispatch(authActions.LOGOUT())
112                 },
113                 {
114                     label: "My account",
115                     action: () => this.props.dispatch(push("/my-account"))
116                 }
117             ],
118             helpMenu: [
119                 {
120                     label: "Help",
121                     action: () => this.props.dispatch(push("/help"))
122                 }
123             ],
124             anonymousMenu: [
125                 {
126                     label: "Sign in",
127                     action: () => this.props.dispatch(authActions.LOGIN())
128                 }
129             ]
130         }
131     };
132
133
134     mainAppBarActions: MainAppBarActionProps = {
135         onBreadcrumbClick: ({ itemId, status }: NavBreadcrumb) => {
136             this.toggleProjectTreeItemOpen(itemId, status);
137         },
138         onSearch: searchText => {
139             this.setState({ searchText });
140             this.props.dispatch(push(`/search?q=${searchText}`));
141         },
142         onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action()
143     };
144
145     toggleProjectTreeItemOpen = (itemId: string, status: TreeItemStatus) => {
146         if (status === TreeItemStatus.Loaded) {
147             this.openProjectItem(itemId);
148             this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(itemId));
149             this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
150         } else {
151             this.props.dispatch<any>(getProjectList(itemId))
152                 .then(() => {
153                     this.openProjectItem(itemId);
154                     this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(itemId));
155                     this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
156                 });
157         }
158     }
159
160     toggleProjectTreeItemActive = (itemId: string, status: TreeItemStatus) => {
161         if (status === TreeItemStatus.Loaded) {
162             this.openProjectItem(itemId);
163             this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
164             this.props.dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(itemId));
165         } else {
166             this.props.dispatch<any>(getProjectList(itemId))
167                 .then(() => {
168                     this.openProjectItem(itemId);
169                     this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
170                     this.props.dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(itemId));
171                 });
172         }
173     }
174
175     toggleSidePanelOpen = (itemId: string) => {
176         this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
177     }
178
179     toggleSidePanelActive = (itemId: string) => {
180         this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
181         this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
182     }
183
184     openProjectItem = (itemId: string) => {
185         const branch = getTreePath(this.props.projects, itemId);
186         this.setState({
187             breadcrumbs: branch.map(item => ({
188                 label: item.data.name,
189                 itemId: item.data.uuid,
190                 status: item.status
191             }))
192         });
193         this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
194         this.props.dispatch(push(`/project/${itemId}`));
195
196         const project = findTreeItem(this.props.projects, itemId);
197         const items: ProjectExplorerItem[] = project && project.items
198             ? project.items.map(({ data }) => ({
199                 uuid: data.uuid,
200                 name: data.name,
201                 type: data.kind,
202                 owner: data.ownerUuid,
203                 lastModified: data.modifiedAt
204             }))
205             : [];
206         this.props.dispatch(dataExplorerActions.SET_ITEMS({ id: PROJECT_EXPLORER_ID, items }));
207     }
208
209     render() {
210         const { classes, user, projects, sidePanelItems } = this.props;
211         return (
212             <div className={classes.root}>
213                 <div className={classes.appBar}>
214                     <MainAppBar
215                         breadcrumbs={this.state.breadcrumbs}
216                         searchText={this.state.searchText}
217                         user={this.props.user}
218                         menuItems={this.state.menuItems}
219                         {...this.mainAppBarActions}
220                     />
221                 </div>
222                 {user &&
223                     <Drawer
224                         variant="permanent"
225                         classes={{
226                             paper: classes.drawerPaper,
227                         }}>
228                         <div className={classes.toolbar} />
229                         <SidePanel
230                             toggleOpen={this.toggleSidePanelOpen}
231                             toggleActive={this.toggleSidePanelActive}
232                             sidePanelItems={sidePanelItems}>
233                             <ProjectTree
234                                 projects={projects}
235                                 toggleOpen={this.toggleProjectTreeItemOpen}
236                                 toggleActive={this.toggleProjectTreeItemActive} />
237                         </SidePanel>
238                     </Drawer>}
239                 <main className={classes.contentWrapper}>
240                     <div className={classes.content}>
241                         <Switch>
242                             <Route path="/project/:name" component={ProjectPanel} />
243                         </Switch>
244                     </div>
245                 </main>
246             </div>
247         );
248     }
249 }
250
251 export default connect<WorkbenchDataProps>(
252     (state: RootState) => ({
253         projects: state.projects,
254         user: state.auth.user,
255         sidePanelItems: state.sidePanel,
256     })
257 )(
258     withStyles(styles)(Workbench)
259 );