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