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