21702: Merge branch 'main' into 21702-keep-web-replace_files
[arvados.git] / services / workbench2 / src / views-components / 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 { CustomStyleRulesCallback } from 'common/custom-theme';
7 import { Card, CardHeader, Typography, CardContent, Tooltip, Collapse, Grid } from '@mui/material';
8 import { WithStyles } from '@mui/styles';
9 import withStyles from '@mui/styles/withStyles';
10 import { ArvadosTheme } from 'common/custom-theme';
11 import { RootState } from 'store/store';
12 import { connect } from 'react-redux';
13 import { getResource } from 'store/resources/resources';
14 import { getPropertyChip } from '../resource-properties-form/property-chip';
15 import { ProjectResource } from 'models/project';
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     | 'chipToggle'
40     | 'chipSection'
41     | 'tag'
42     | 'description'
43     | 'toolbarStyles';
44
45 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
46     root: {
47         width: '100%',
48         marginBottom: '1rem',
49         flex: '0 0 auto',
50         padding: 0,
51         minHeight: '3rem',
52     },
53     noDescription: {
54         color: theme.palette.grey['600'],
55         fontStyle: 'italic',
56         marginLeft: '2rem',
57     },
58     userNameContainer: {
59         display: 'flex',
60         alignItems: 'center',
61         minHeight: '2.7rem',
62     },
63     cardHeaderContainer: {
64         width: '100%',
65         display: 'flex',
66         flexDirection: 'row',
67         alignItems: 'center',
68         justifyContent: 'space-between',
69     },
70     cardHeader: {
71         minWidth: '30rem',
72         padding: '0.2rem 0.4rem 0.2rem 1rem',
73     },
74     descriptionToggle: {
75         display: 'flex',
76         flexDirection: 'row',
77         cursor: 'pointer',
78         marginTop: '-0.25rem',
79         paddingBottom: '0.5rem',
80     },
81     cardContent: {
82         display: 'flex',
83         flexDirection: 'column',
84         paddingTop: 0,
85         paddingLeft: '0.1rem',
86     },
87     nameSection: {
88         display: 'flex',
89         flexDirection: 'row',
90         alignItems: 'center',
91     },
92     namePlate: {
93         display: 'flex',
94         flexDirection: 'row',
95         alignItems: 'center',
96         margin: 0,
97         minHeight: '2.7rem',
98     },
99     faveIcon: {
100         fontSize: '0.8rem',
101         margin: 'auto 0 1rem 0.3rem',
102         color: theme.palette.text.primary,
103     },
104     frozenIcon: {
105         fontSize: '0.5rem',
106         marginLeft: '0.3rem',
107         height: '1rem',
108         color: theme.palette.text.primary,
109     },
110     showMore: {
111         marginTop: 0,
112         cursor: 'pointer',
113     },
114     chipToggle: {
115         display: 'flex',
116         alignItems: 'center',
117         height: '2rem',
118     },
119     chipSection: {
120         marginBottom: '-1rem',
121     },
122     tag: {
123         marginRight: '0.75rem',
124         marginBottom: '0.5rem',
125     },
126     description: {
127         marginTop: 0,
128         marginRight: '2rem',
129         marginBottom: '-0.85rem',
130     },
131     toolbarStyles: {
132         marginRight: '-0.5rem',
133     },
134 });
135
136 const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => {
137     const currentResource = getResource(properties.currentRouteUuid)(resources);
138     const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
139     const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
140     const isSelected = selectedResourceUuid === properties.currentRouteUuid;
141
142     return {
143         isAdmin: auth.user?.isAdmin,
144         currentResource,
145         frozenByFullName,
146         isSelected,
147     };
148 };
149
150 const mapDispatchToProps = (dispatch: Dispatch) => ({
151     handleCardClick: (uuid: string) => {
152         dispatch<any>(loadDetailsPanel(uuid));
153         dispatch<any>(setSelectedResourceUuid(uuid));
154         dispatch<any>(deselectAllOthers(uuid));
155     },
156 });
157
158 type ProjectCardProps = WithStyles<CssRules> & {
159     currentResource: ProjectResource;
160     frozenByFullName: string | undefined;
161     isAdmin: boolean;
162     isSelected: boolean;
163     handleCardClick: (resource: any) => void;
164 };
165
166 export const ProjectCard = connect(
167     mapStateToProps,
168     mapDispatchToProps
169 )(
170     withStyles(styles)((props: ProjectCardProps) => {
171         const { classes, currentResource, frozenByFullName, handleCardClick, isSelected } = props;
172         const { name, description, uuid } = currentResource as ProjectResource;
173         const [showDescription, setShowDescription] = React.useState(false);
174         const [showProperties, setShowProperties] = React.useState(false);
175
176         const toggleDescription = () => {
177             setShowDescription(!showDescription);
178         };
179
180         const toggleProperties = () => {
181             setShowProperties(!showProperties);
182         };
183
184         return (
185             <Card
186                 className={classes.root}
187                 onClick={() => handleCardClick(uuid)}
188                 data-cy='project-details-card'
189             >
190                 <Grid
191                     container
192                     wrap='nowrap'
193                     className={classes.cardHeaderContainer}
194                 >
195                     <CardHeader
196                         className={classes.cardHeader}
197                         title={
198                             <section className={classes.nameSection}>
199                                 <section className={classes.namePlate}>
200                                     <Typography
201                                         variant='h6'
202                                         style={{ marginRight: '1rem' }}
203                                     >
204                                         {name}
205                                     </Typography>
206                                     <FavoriteStar
207                                         className={classes.faveIcon}
208                                         resourceUuid={currentResource.uuid}
209                                     />
210                                     <PublicFavoriteStar
211                                         className={classes.faveIcon}
212                                         resourceUuid={currentResource.uuid}
213                                     />
214                                     {!!frozenByFullName && (
215                                         <Tooltip
216                                             className={classes.frozenIcon}
217                                             disableFocusListener
218                                             title={<span>Project was frozen by {frozenByFullName}</span>}
219                                         >
220                                             <FreezeIcon style={{ fontSize: 'inherit' }} />
221                                         </Tooltip>
222                                     )}
223                                 </section>
224                                 {!description && (
225                                     <Typography
226                                         data-cy='no-description'
227                                         className={classes.noDescription}
228                                     >
229                                         no description available
230                                     </Typography>
231                                 )}
232                             </section>
233                         }
234                     />
235                     {isSelected && <MultiselectToolbar injectedStyles={classes.toolbarStyles} />}
236                 </Grid>
237                 <section onClick={(ev) => ev.stopPropagation()}>
238                     {description ? (
239                         <section
240                             onClick={toggleDescription}
241                             className={classes.descriptionToggle}
242                             data-cy='toggle-description'
243                         >
244                             <ExpandChevronRight expanded={showDescription} />
245                             <section className={classes.showMore}>
246                                 <Collapse
247                                     in={showDescription}
248                                     timeout='auto'
249                                     collapsedSize='1.25rem'
250                                 >
251                                     <Typography
252                                         className={classes.description}
253                                         data-cy='project-description'
254                                         //dangerouslySetInnerHTML is ok here only if description is sanitized,
255                                         //which it is before it is loaded into the redux store
256                                         dangerouslySetInnerHTML={{ __html: description }}
257                                     />
258                                 </Collapse>
259                             </section>
260                         </section>
261                     ) : (
262                         <></>
263                     )}
264                     {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
265                         <section
266                             onClick={toggleProperties}
267                             className={classes.descriptionToggle}
268                         >
269                             <div
270                                 className={classes.chipToggle}
271                                 data-cy='toggle-chips'
272                             >
273                                 <ExpandChevronRight expanded={showProperties} />
274                             </div>
275                             <section className={classes.showMore}>
276                                 <Collapse
277                                     in={showProperties}
278                                     timeout='auto'
279                                     collapsedSize='35px'
280                                 >
281                                     <div
282                                         className={classes.description}
283                                         data-cy='project-description'
284                                     >
285                                         <CardContent className={classes.cardContent}>
286                                             <Typography
287                                                 component='div'
288                                                 className={classes.chipSection}
289                                             >
290                                                 {Object.keys(currentResource.properties).map((k) =>
291                                                     Array.isArray(currentResource.properties[k])
292                                                         ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
293                                                         : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
294                                                 )}
295                                             </Typography>
296                                         </CardContent>
297                                     </div>
298                                 </Collapse>
299                             </section>
300                         </section>
301                     ) : null}
302                 </section>
303             </Card>
304         );
305     })
306 );