61aa0be519413ba6b5da590ab039a96631f5e0ba
[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, Grid } 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 { FreezeIcon } from 'components/icon/icon';
18 import { Resource } from 'models/resource';
19 import { ContextMenuResource } from 'store/context-menu/context-menu-actions';
20 import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
21 import { CollectionResource } from 'models/collection';
22 import { ContextMenuKind } from 'views-components/context-menu/context-menu';
23 import { Dispatch } from 'redux';
24 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
25 import { ExpandChevronRight } from 'components/expand-chevron-right/expand-chevron-right';
26 import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar';
27
28 type CssRules =
29     | 'root'
30     | 'cardHeaderContainer'
31     | 'cardHeader'
32     | 'descriptionToggle'
33     | 'showMore'
34     | 'noDescription'
35     | 'userNameContainer'
36     | 'cardContent'
37     | 'nameSection'
38     | 'namePlate'
39     | 'faveIcon'
40     | 'frozenIcon'
41     | 'accountStatusSection'
42     | 'toolbarSection'
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         padding: 0,
52         border: '2px solid transparent',
53     },
54     showMore: {
55         cursor: 'pointer',
56         background: 'linear-gradient(to right, black, transparent)',
57         backgroundClip: 'text',
58         color: 'transparent',
59     },
60     noDescription: {
61         color: theme.palette.grey['600'],
62         fontStyle: 'italic',
63     },
64     userNameContainer: {
65         display: 'flex',
66         alignItems: 'center',
67     },
68     cardHeaderContainer: {
69         width: '100%',
70         display: 'flex',
71         flexDirection: 'row',
72         justifyContent: 'space-between',
73     },
74     cardHeader: {
75         minWidth: '40rem',
76         padding: '0.2rem 0.4rem 0.1rem 1rem',
77     },
78     descriptionToggle: {
79         display: 'flex',
80         flexDirection: 'row',
81         cursor: 'pointer',
82         paddingBottom: '0.5rem',
83     },
84     cardContent: {
85         display: 'flex',
86         flexDirection: 'column',
87         marginTop: '-1.75rem',
88     },
89     nameSection: {
90         display: 'flex',
91         flexDirection: 'row',
92         alignItems: 'center',
93         justifyContent: 'space-between',
94     },
95     namePlate: {
96         display: 'flex',
97         flexDirection: 'row',
98         alignItems: 'center',
99         margin: 0,
100         paddingBottom: '0.5rem',
101     },
102     faveIcon: {
103         fontSize: '0.8rem',
104         margin: 'auto 0 0.5rem 0.3rem',
105         color: theme.palette.text.primary,
106     },
107     frozenIcon: {
108         fontSize: '0.5rem',
109         marginLeft: '0.3rem',
110         marginTop: '0.1rem',
111         height: '1rem',
112         color: theme.palette.text.primary,
113     },
114     accountStatusSection: {
115         display: 'flex',
116         flexDirection: 'row',
117         alignItems: 'center',
118         paddingLeft: '1rem',
119     },
120     toolbarSection: {
121         marginTop: '-1rem',
122         paddingBottom: '-1rem',
123     },
124     tag: {
125         marginRight: '1rem',
126         marginTop: '1rem',
127     },
128     description: {
129         maxWidth: '95%',
130         marginTop: 0,
131     },
132 });
133
134 const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => {
135     const currentResource = getResource(properties.currentRouteUuid)(resources);
136     const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
137     const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
138     const isSelected = selectedResourceUuid === properties.currentRouteUuid;
139
140     return {
141         isAdmin: auth.user?.isAdmin,
142         currentResource,
143         frozenByFullName,
144         isSelected,
145     };
146 };
147
148 const mapDispatchToProps = (dispatch: Dispatch) => ({
149     handleCardClick: (uuid: string) => {
150         dispatch<any>(loadDetailsPanel(uuid));
151     },
152     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: any, isAdmin: boolean) => {
153         event.stopPropagation();
154         // When viewing the contents of a filter group, all contents should be treated as read only.
155         let readOnly = false;
156         if (resource.groupClass === 'filter') {
157             readOnly = true;
158         }
159         let menuKind = dispatch<any>(resourceUuidToContextMenuKind(resource.uuid, readOnly));
160         if (menuKind === ContextMenuKind.ROOT_PROJECT) {
161             menuKind = ContextMenuKind.USER_DETAILS;
162         }
163         if (menuKind && resource) {
164             dispatch<any>(
165                 openContextMenu(event, {
166                     name: resource.name,
167                     uuid: resource.uuid,
168                     ownerUuid: resource.ownerUuid,
169                     isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
170                     kind: resource.kind,
171                     menuKind,
172                     isAdmin,
173                     isFrozen: !!resource.frozenByUuid,
174                     description: resource.description,
175                     storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
176                     properties: 'properties' in resource ? resource.properties : {},
177                 })
178             );
179         }
180     },
181 });
182
183 type DetailsCardProps = WithStyles<CssRules> & {
184     currentResource: ProjectResource | UserResource;
185     frozenByFullName?: string;
186     isAdmin: boolean;
187     isSelected: boolean;
188     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
189     handleCardClick: (resource: any) => void;
190 };
191
192 type UserCardProps = WithStyles<CssRules> & {
193     currentResource: UserResource;
194     isAdmin: boolean;
195     isSelected: boolean;
196     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
197     handleCardClick: (resource: any) => void;
198 };
199
200 type ProjectCardProps = WithStyles<CssRules> & {
201     currentResource: ProjectResource;
202     frozenByFullName: string | undefined;
203     isAdmin: boolean;
204     isSelected: boolean;
205     handleCardClick: (resource: any) => void;
206 };
207
208 export const ProjectDetailsCard = connect(
209     mapStateToProps,
210     mapDispatchToProps
211 )(
212     withStyles(styles)((props: DetailsCardProps) => {
213         const { classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected } = props;
214         if (!currentResource) {
215             return null;
216         }
217         switch (currentResource.kind as string) {
218             case ResourceKind.USER:
219                 return (
220                     <UserCard
221                         classes={classes}
222                         currentResource={currentResource as UserResource}
223                         isAdmin={isAdmin}
224                         isSelected={isSelected}
225                         handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
226                         handleCardClick={handleCardClick}
227                     />
228                 );
229             case ResourceKind.PROJECT:
230                 return (
231                     <ProjectCard
232                         classes={classes}
233                         currentResource={currentResource as ProjectResource}
234                         frozenByFullName={frozenByFullName}
235                         isAdmin={isAdmin}
236                         isSelected={isSelected}
237                         handleCardClick={handleCardClick}
238                     />
239                 );
240             default:
241                 return null;
242         }
243     })
244 );
245
246 const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
247     const { fullName, uuid } = currentResource as UserResource & { fullName: string };
248
249     return (
250         <Card
251             className={classes.root}
252             onClick={() => handleCardClick(uuid)}
253             data-cy='user-details-card'
254         >
255             <Grid
256                 container
257                 wrap='nowrap'
258                 className={classes.cardHeaderContainer}
259             >
260                 <CardHeader
261                     className={classes.cardHeader}
262                     title={
263                         <section className={classes.userNameContainer}>
264                             <Typography
265                                 noWrap
266                                 variant='h6'
267                             >
268                                 {fullName}
269                             </Typography>
270                             <section className={classes.accountStatusSection}>
271                                 {!currentResource.isActive && (
272                                     <Typography>
273                                         <UserResourceAccountStatus uuid={uuid} />
274                                     </Typography>
275                             )}
276                             </section>
277                         </section>
278                     }
279                 />
280                 {isSelected && <MultiselectToolbar />}
281             </Grid>
282         </Card>
283     );
284 };
285
286 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleCardClick, isSelected }) => {
287     const { name, description, uuid } = currentResource as ProjectResource;
288     const [showDescription, setShowDescription] = React.useState(false);
289     const [showProperties, setShowProperties] = React.useState(false);
290
291     const toggleDescription = () => {
292         setShowDescription(!showDescription);
293     };
294
295     const toggleProperties = () => {
296         setShowProperties(!showProperties);
297     };
298
299     return (
300         <Card
301             className={classes.root}
302             onClick={() => handleCardClick(uuid)}
303             data-cy='project-details-card'
304         >
305             <Grid
306                 container
307                 wrap='nowrap'
308                 className={classes.cardHeaderContainer}
309             >
310                 <CardHeader
311                     className={classes.cardHeader}
312                     title={
313                         <section className={classes.nameSection}>
314                             <section className={classes.namePlate}>
315                                 <Typography
316                                     variant='h6'
317                                     style={{ marginRight: '1rem' }}
318                                 >
319                                     {name}
320                                 </Typography>
321                                 <FavoriteStar
322                                     className={classes.faveIcon}
323                                     resourceUuid={currentResource.uuid}
324                                 />
325                                 <PublicFavoriteStar
326                                     className={classes.faveIcon}
327                                     resourceUuid={currentResource.uuid}
328                                 />
329                                 {!!frozenByFullName && (
330                                     <Tooltip
331                                         className={classes.frozenIcon}
332                                         disableFocusListener
333                                         title={<span>Project was frozen by {frozenByFullName}</span>}
334                                     >
335                                         <FreezeIcon style={{ fontSize: 'inherit' }} />
336                                     </Tooltip>
337                                 )}
338                             </section>
339                         </section>
340                     }
341                 />
342                 {isSelected && <MultiselectToolbar />}
343             </Grid>
344             <section onClick={(ev) => ev.stopPropagation()}>
345                 {description ? (
346                     <section
347                         onClick={toggleDescription}
348                         className={classes.descriptionToggle}
349                     >
350                         <ExpandChevronRight expanded={showDescription} />
351                         <section className={classes.showMore}>
352                             <Collapse
353                                 in={showDescription}
354                                 timeout='auto'
355                                 collapsedHeight='1.25rem'
356                             >
357                                 <Typography
358                                     className={classes.description}
359                                     data-cy='project-description'
360                                 >
361                                     {description}
362                                 </Typography>
363                             </Collapse>
364                         </section>
365                     </section>
366                 ) : (
367                     <Typography
368                         className={classes.noDescription}
369                         data-cy='no-description'
370                     >
371                         no description available
372                     </Typography>
373                 )}
374                 {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
375                     <section
376                         onClick={toggleProperties}
377                         className={classes.descriptionToggle}
378                     >
379                         <ExpandChevronRight expanded={showProperties} />
380                         <section className={classes.showMore}>
381                             <Collapse
382                                 in={showProperties}
383                                 timeout='auto'
384                                 collapsedHeight='35px'
385                             >
386                                 <div
387                                     className={classes.description}
388                                     data-cy='project-description'
389                                 >
390                                     <CardContent className={classes.cardContent}>
391                                         <Typography component='div'>
392                                             {Object.keys(currentResource.properties).map((k) =>
393                                                 Array.isArray(currentResource.properties[k])
394                                                     ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
395                                                     : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
396                                             )}
397                                         </Typography>
398                                     </CardContent>
399                                 </div>
400                             </Collapse>
401                         </section>
402                     </section>
403                 ) : null}
404             </section>
405         </Card>
406     );
407 };