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