21224: allowed for getResource fail Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa...
[arvados.git] / services / workbench2 / src / views-components / project-details-card / project-details-card.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 { StyleRulesCallback, Card, CardHeader, WithStyles, withStyles, Typography, CardContent, Tooltip, Collapse } from '@material-ui/core';
7 import { ArvadosTheme } from 'common/custom-theme';
8 import { RootState } from 'store/store';
9 import { connect } from 'react-redux';
10 import { getResource } from 'store/resources/resources';
11 import { getPropertyChip } from '../resource-properties-form/property-chip';
12 import { ProjectResource } from 'models/project';
13 import { ResourceKind } from 'models/resource';
14 import { UserResource } from 'models/user';
15 import { UserResourceAccountStatus } from 'views-components/data-explorer/renderers';
16 import { FavoriteStar, PublicFavoriteStar } from 'views-components/favorite-star/favorite-star';
17 import { MoreVerticalIcon, FreezeIcon } from 'components/icon/icon';
18 import { Resource } from 'models/resource';
19 import { IconButton } from '@material-ui/core';
20 import { ContextMenuResource, openUserContextMenu } from 'store/context-menu/context-menu-actions';
21 import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
22 import { CollectionResource } from 'models/collection';
23 import { ContextMenuKind } from 'views-components/context-menu/context-menu';
24 import { Dispatch } from 'redux';
25 import classNames from 'classnames';
26 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
27
28 type CssRules =
29     | 'root'
30     | 'selected'
31     | 'cardHeader'
32     | 'descriptionLabel'
33     | 'showMore'
34     | 'noDescription'
35     | 'nameContainer'
36     | 'cardContent'
37     | 'subHeader'
38     | 'namePlate'
39     | 'faveIcon'
40     | 'frozenIcon'
41     | 'contextMenuSection'
42     | 'chipSection'
43     | 'tag'
44     | 'description';
45
46 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
47     root: {
48         width: '100%',
49         marginBottom: '1rem',
50         flex: '0 0 auto',
51         paddingTop: '0.2rem',
52         border: '2px solid transparent',
53     },
54     selected: {
55         border: '2px solid #ccc',
56     },
57     showMore: {
58         color: theme.palette.primary.main,
59         cursor: 'pointer',
60     },
61     noDescription: {
62         color: theme.palette.grey['600'],
63         fontStyle: 'italic',
64     },
65     nameContainer: {
66         display: 'flex',
67     },
68     cardHeader: {
69         paddingTop: '0.4rem',
70     },
71     descriptionLabel: {
72         paddingTop: '1rem',
73         marginBottom: 0,
74         minHeight: '2.5rem',
75         marginRight: '0.8rem',
76     },
77     cardContent: {
78         display: 'flex',
79         flexDirection: 'column',
80         transition: 'height 0.3s ease',
81     },
82     subHeader: {
83         display: 'flex',
84         flexDirection: 'row',
85         justifyContent: 'space-between',
86         marginTop: '-2rem',
87     },
88     namePlate: {
89         display: 'flex',
90         flexDirection: 'row',
91     },
92     faveIcon: {
93         fontSize: '0.8rem',
94         margin: 'auto 0 0.5rem 0.3rem',
95         color: theme.palette.text.primary,
96     },
97     frozenIcon: {
98         fontSize: '0.5rem',
99         marginLeft: '0.3rem',
100         marginTop: '0.57rem',
101         height: '1rem',
102         color: theme.palette.text.primary,
103     },
104     contextMenuSection: {
105         display: 'flex',
106         flexDirection: 'row',
107         alignItems: 'center',
108         marginTop: '0.6rem',
109     },
110     chipSection: {
111         display: 'flex',
112         flexWrap: 'wrap',
113     },
114     tag: {
115         marginRight: '1rem',
116         marginTop: '0.5rem',
117     },
118     description: {
119         marginTop: '1rem',
120     },
121 });
122
123 const mapStateToProps = (state: RootState) => {
124     const currentRoute = state.router.location?.pathname.split('/') || [];
125     const currentItemUuid = currentRoute[currentRoute.length - 1];
126     const currentResource = getResource(currentItemUuid)(state.resources);
127     const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(state.resources);
128     const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
129     const isSelected = currentItemUuid === state.detailsPanel.resourceUuid && state.detailsPanel.isOpened === true;
130
131     return {
132         isAdmin: state.auth.user?.isAdmin,
133         currentResource,
134         frozenByFullName,
135         isSelected,
136     };
137 };
138
139 const mapDispatchToProps = (dispatch: Dispatch) => ({
140     handleCardClick: (uuid: string) => {
141         dispatch<any>(loadDetailsPanel(uuid));
142     },
143     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: any, isAdmin: boolean) => {
144         event.stopPropagation();
145         // When viewing the contents of a filter group, all contents should be treated as read only.
146         let readOnly = false;
147         if (resource.groupClass === 'filter') {
148             readOnly = true;
149         }
150         const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resource.uuid, readOnly));
151         if (menuKind === ContextMenuKind.ROOT_PROJECT) {
152             dispatch<any>(openUserContextMenu(event, resource as UserResource));
153         } else if (menuKind && resource) {
154             dispatch<any>(
155                 openContextMenu(event, {
156                     name: resource.name,
157                     uuid: resource.uuid,
158                     ownerUuid: resource.ownerUuid,
159                     isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
160                     kind: resource.kind,
161                     menuKind,
162                     isAdmin,
163                     isFrozen: !!resource.frozenByUuid,
164                     description: resource.description,
165                     storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
166                     properties: 'properties' in resource ? resource.properties : {},
167                 })
168             );
169         }
170     },
171 });
172
173 type DetailsCardProps = WithStyles<CssRules> & {
174     currentResource: ProjectResource | UserResource;
175     frozenByFullName?: string;
176     isAdmin: boolean;
177     isSelected: boolean;
178     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
179     handleCardClick: (resource: any) => void;
180 };
181
182 type UserCardProps = WithStyles<CssRules> & {
183     currentResource: UserResource;
184     isAdmin: boolean;
185     isSelected: boolean;
186     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
187     handleCardClick: (resource: any) => void;
188 };
189
190 type ProjectCardProps = WithStyles<CssRules> & {
191     currentResource: ProjectResource;
192     frozenByFullName: string | undefined;
193     isAdmin: boolean;
194     isSelected: boolean;
195     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
196     handleCardClick: (resource: any) => void;
197 };
198
199 export const ProjectDetailsCard = connect(
200     mapStateToProps,
201     mapDispatchToProps
202 )(
203     withStyles(styles)((props: DetailsCardProps) => {
204         const { classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected } = props;
205         if (!currentResource) {
206             return null;
207         }
208         switch (currentResource.kind as string) {
209             case ResourceKind.USER:
210                 return (
211                     <UserCard
212                         classes={classes}
213                         currentResource={currentResource as UserResource}
214                         isAdmin={isAdmin}
215                         isSelected={isSelected}
216                         handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
217                         handleCardClick={handleCardClick}
218                     />
219                 );
220             case ResourceKind.PROJECT:
221                 return (
222                     <ProjectCard
223                         classes={classes}
224                         currentResource={currentResource as ProjectResource}
225                         frozenByFullName={frozenByFullName}
226                         isAdmin={isAdmin}
227                         isSelected={isSelected}
228                         handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
229                         handleCardClick={handleCardClick}
230                     />
231                 );
232             default:
233                 return null;
234         }
235     })
236 );
237
238 const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
239     const { fullName, uuid } = currentResource as UserResource & { fullName: string };
240
241     return (
242         <Card className={classNames(classes.root, isSelected ? classes.selected : '')} onClick={()=>handleCardClick(uuid)}>
243             <CardHeader
244                 className={classes.cardHeader}
245                 title={
246                     <section className={classes.nameContainer}>
247                         <Typography
248                             noWrap
249                             variant='h6'
250                         >
251                             {fullName}
252                         </Typography>
253                     </section>
254                 }
255                 action={
256                     <section className={classes.contextMenuSection}>
257                         {!currentResource.isActive && (
258                             <Typography>
259                                 <UserResourceAccountStatus uuid={uuid} />
260                             </Typography>
261                         )}
262                         <Tooltip
263                             title='More options'
264                             disableFocusListener
265                         >
266                             <IconButton
267                                 aria-label='More options'
268                                 onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
269                             >
270                                 <MoreVerticalIcon />
271                             </IconButton>
272                         </Tooltip>
273                     </section>
274                 }
275             />
276         </Card>
277     );
278 };
279
280 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
281     const { name, description, uuid } = currentResource as ProjectResource;
282     const [showDescription, setShowDescription] = React.useState(false);
283
284     const toggleDescription = () => {
285         setShowDescription(!showDescription);
286     };
287
288     return (
289         <Card className={classNames(classes.root, isSelected ? classes.selected : '')} onClick={()=>handleCardClick(uuid)}>
290             <CardHeader
291                 className={classes.cardHeader}
292                 title={
293                     <section className={classes.namePlate}>
294                         <Typography
295                             noWrap
296                             variant='h6'
297                             style={{ marginRight: '1rem' }}
298                         >
299                             {name}
300                         </Typography>
301                         <FavoriteStar
302                             className={classes.faveIcon}
303                             resourceUuid={currentResource.uuid}
304                         />
305                         <PublicFavoriteStar
306                             className={classes.faveIcon}
307                             resourceUuid={currentResource.uuid}
308                         />
309                         {!!frozenByFullName && (
310                             <Tooltip
311                                 className={classes.frozenIcon}
312                                 title={<span>Project was frozen by {frozenByFullName}</span>}
313                             >
314                                 <FreezeIcon style={{ fontSize: 'inherit' }} />
315                             </Tooltip>
316                         )}
317                     </section>
318                 }
319                 action={
320                     <section className={classes.contextMenuSection}>
321                         <Tooltip
322                             title='More options'
323                             disableFocusListener
324                         >
325                             <IconButton
326                                 aria-label='More options'
327                                 onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
328                             >
329                                 <MoreVerticalIcon />
330                             </IconButton>
331                         </Tooltip>
332                     </section>
333                 }
334             />
335             <CardContent className={classes.cardContent}>
336                 <section className={classes.subHeader}>
337                     <section className={classes.chipSection}>
338                         <Typography component='div'>
339                             {typeof currentResource.properties === 'object' &&
340                                 Object.keys(currentResource.properties).map((k) =>
341                                     Array.isArray(currentResource.properties[k])
342                                         ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
343                                         : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
344                                 )}
345                         </Typography>
346                     </section>
347                     <section className={classes.descriptionLabel} onClick={(ev)=>ev.stopPropagation()}>
348                         {description ? (
349                             <Typography
350                                 className={classes.showMore}
351                                 onClick={toggleDescription}
352                             >
353                                 {!showDescription ? "Show full description" : "Hide full description"}
354                             </Typography>
355                         ) : (
356                             <Typography className={classes.noDescription}>no description available</Typography>
357                         )}
358                     </section>
359                 </section>
360                 <Collapse in={showDescription} timeout='auto'>
361                     <section onClick={(ev)=>ev.stopPropagation()}>
362                         <Typography className={classes.description}>
363                             {description}
364                         </Typography>
365                     </section>
366                 </Collapse>
367             </CardContent>
368         </Card>
369     );
370 };