21720: fixed project test
[arvados.git] / services / workbench2 / src / views-components / side-panel-button / side-panel-button.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { connect, DispatchProp } from 'react-redux';
7 import { RootState } from 'store/store';
8 import { ArvadosTheme } from 'common/custom-theme';
9 import { PopoverOrigin } from '@mui/material/Popover';
10 import { CustomStyleRulesCallback } from 'common/custom-theme';
11 import { Toolbar, Grid, Button, MenuItem, Menu } from '@mui/material';
12 import { WithStyles } from '@mui/styles';
13 import withStyles from '@mui/styles/withStyles';
14 import { AddIcon, CollectionIcon, ProcessIcon, ProjectIcon } from 'components/icon/icon';
15 import { openProjectCreateDialog } from 'store/projects/project-create-actions';
16 import { openCollectionCreateDialog } from 'store/collections/collection-create-actions';
17 import { navigateToRunProcess } from 'store/navigation/navigation-action';
18 import { runProcessPanelActions } from 'store/run-process-panel/run-process-panel-actions';
19 import { getUserUuid } from 'common/getuser';
20 import { matchProjectRoute } from 'routes/routes';
21 import { GroupClass, GroupResource } from 'models/group';
22 import { ResourcesState, getResource } from 'store/resources/resources';
23 import { extractUuidKind, ResourceKind } from 'models/resource';
24 import { pluginConfig } from 'plugins';
25 import { ElementListReducer } from 'common/plugintypes';
26 import { Location } from 'history';
27 import { ProjectResource } from 'models/project';
28
29 type CssRules = 'button' | 'menuItem' | 'icon';
30
31 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
32     button: {
33         boxShadow: 'none',
34         padding: '2px 10px 2px 5px',
35         fontSize: '0.75rem'
36     },
37     menuItem: {
38         fontSize: '0.875rem',
39         color: theme.palette.grey["700"]
40     },
41     icon: {
42         marginRight: theme.spacing(1)
43     }
44 });
45
46 interface SidePanelDataProps {
47     location: Location;
48     currentItemId: string;
49     resources: ResourcesState;
50     currentUserUUID: string | undefined;
51 }
52
53 interface SidePanelState {
54     anchorEl: any;
55 }
56
57 type SidePanelProps = SidePanelDataProps & DispatchProp & WithStyles<CssRules>;
58
59 const transformOrigin: PopoverOrigin = {
60     vertical: -50,
61     horizontal: 0
62 };
63
64 export const isProjectTrashed = (proj: GroupResource | undefined, resources: ResourcesState): boolean => {
65     if (proj === undefined) { return false; }
66     if (proj.isTrashed) { return true; }
67     if (extractUuidKind(proj.ownerUuid) === ResourceKind.USER) { return false; }
68     const parentProj = getResource<GroupResource>(proj.ownerUuid)(resources);
69     return isProjectTrashed(parentProj, resources);
70 };
71
72 export const SidePanelButton = withStyles(styles)(
73     connect((state: RootState) => ({
74         currentItemId: state.router.location
75             ? state.router.location.pathname.split('/').slice(-1)[0]
76             : null,
77         location: state.router.location,
78         resources: state.resources,
79         currentUserUUID: getUserUuid(state),
80     }))(
81         class extends React.Component<SidePanelProps> {
82
83             state: SidePanelState = {
84                 anchorEl: undefined
85             };
86
87             render() {
88                 const { classes, location, resources, currentUserUUID, currentItemId } = this.props;
89                 const { anchorEl } = this.state;
90                 let enabled = false;
91                 if (currentItemId === currentUserUUID) {
92                     enabled = true;
93                 } else if (matchProjectRoute(location ? location.pathname : '')) {
94                     const currentProject = getResource<ProjectResource>(currentItemId)(resources);
95                     if (currentProject && currentProject.canWrite &&
96                         !currentProject.frozenByUuid &&
97                         !isProjectTrashed(currentProject, resources) &&
98                         currentProject.groupClass !== GroupClass.FILTER) {
99                         enabled = true;
100                     }
101                 }
102
103                 for (const enableFn of pluginConfig.enableNewButtonMatchers) {
104                     if (enableFn(location, currentItemId, currentUserUUID, resources)) {
105                         enabled = true;
106                     }
107                 }
108
109                 let menuItems = <>
110                     <MenuItem data-cy='side-panel-new-collection' className={classes.menuItem} onClick={this.handleNewCollectionClick}>
111                         <CollectionIcon className={classes.icon} /> New collection
112                     </MenuItem>
113                     <MenuItem data-cy='side-panel-run-process' className={classes.menuItem} onClick={this.handleRunProcessClick}>
114                         <ProcessIcon className={classes.icon} /> Run a workflow
115                     </MenuItem>
116                     <MenuItem data-cy='side-panel-new-project' className={classes.menuItem} onClick={this.handleNewProjectClick}>
117                         <ProjectIcon className={classes.icon} /> New project
118                     </MenuItem>
119                 </>;
120
121                 const reduceItemsFn: (a: React.ReactElement[], b: ElementListReducer) => React.ReactElement[] =
122                     (a, b) => b(a, classes.menuItem);
123
124                 menuItems = React.createElement(React.Fragment, null,
125                     pluginConfig.newButtonMenuList.reduce(reduceItemsFn, React.Children.toArray(menuItems.props.children)));
126
127                 return (
128                     <Toolbar style={{paddingRight: 0}}>
129                         <Grid container>
130                             <Grid container item xs alignItems="center" justifyContent="flex-start">
131                                 <Button data-cy="side-panel-button" variant="contained" disabled={!enabled}
132                                     color="primary" size="small" className={classes.button}
133                                     aria-owns={anchorEl ? 'aside-menu-list' : undefined}
134                                     aria-haspopup="true"
135                                     onClick={this.handleOpen}>
136                                     <AddIcon />
137                                     New
138                                 </Button>
139                                 <Menu
140                                     id='aside-menu-list'
141                                     anchorEl={anchorEl}
142                                     open={Boolean(anchorEl)}
143                                     onClose={this.handleClose}
144                                     onClick={this.handleClose}
145                                     transformOrigin={transformOrigin}>
146                                     {menuItems}
147                                 </Menu>
148                             </Grid>
149                         </Grid>
150                     </Toolbar>
151                 );
152             }
153
154             handleNewProjectClick = () => {
155                 this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
156             }
157
158             handleRunProcessClick = () => {
159                 const location = this.props.location;
160                 this.props.dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL());
161                 this.props.dispatch(runProcessPanelActions.SET_PROCESS_PATHNAME(location.pathname));
162                 this.props.dispatch(runProcessPanelActions.SET_PROCESS_OWNER_UUID(this.props.currentItemId));
163
164                 this.props.dispatch<any>(navigateToRunProcess);
165             }
166
167             handleNewCollectionClick = () => {
168                 this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
169             }
170
171             handleClose = () => {
172                 this.setState({ anchorEl: undefined });
173             }
174
175             handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
176                 this.setState({ anchorEl: event.currentTarget });
177             }
178         }
179     )
180 );