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