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