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