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