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