21224: details button works as expected 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, openUserContextMenu } 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
26 type CssRules =
27     | 'root'
28     | 'cardHeader'
29     | 'descriptionLabel'
30     | 'showMore'
31     | 'noDescription'
32     | 'nameContainer'
33     | 'cardContent'
34     | 'subHeader'
35     | 'namePlate'
36     | 'faveIcon'
37     | 'frozenIcon'
38     | 'contextMenuSection'
39     | 'chipSection'
40     | 'tag'
41     | 'description';
42
43 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
44     root: {
45         width: '100%',
46         marginBottom: '1rem',
47         flex: '0 0 auto',
48         paddingTop: '0.2rem',
49     },
50     showMore: {
51         color: theme.palette.primary.main,
52         cursor: 'pointer',
53     },
54     noDescription: {
55         color: theme.palette.grey['600'],
56         fontStyle: 'italic',
57     },
58     nameContainer: {
59         display: 'flex',
60     },
61     cardHeader: {
62         paddingTop: '0.4rem',
63     },
64     descriptionLabel: {
65         paddingTop: '1rem',
66         marginBottom: 0,
67         minHeight: '2.5rem',
68         marginRight: '0.8rem',
69     },
70     cardContent: {
71         display: 'flex',
72         flexDirection: 'column',
73         transition: 'height 0.3s ease',
74     },
75     subHeader: {
76         display: 'flex',
77         flexDirection: 'row',
78         justifyContent: 'space-between',
79         marginTop: '-2rem',
80     },
81     namePlate: {
82         display: 'flex',
83         flexDirection: 'row',
84     },
85     faveIcon: {
86         fontSize: '0.8rem',
87         margin: 'auto 0 0.5rem 0.3rem',
88         color: theme.palette.text.primary,
89     },
90     frozenIcon: {
91         fontSize: '0.5rem',
92         marginLeft: '0.3rem',
93         marginTop: '0.57rem',
94         height: '1rem',
95         color: theme.palette.text.primary,
96     },
97     contextMenuSection: {
98         display: 'flex',
99         flexDirection: 'row',
100         alignItems: 'center',
101         marginTop: '0.6rem',
102     },
103     chipSection: {
104         display: 'flex',
105         flexWrap: 'wrap',
106     },
107     tag: {
108         marginRight: '1rem',
109         marginTop: '0.5rem',
110     },
111     description: {
112         marginTop: '1rem',
113     },
114 });
115
116 const mapStateToProps = (state: RootState) => {
117     const currentRoute = state.router.location?.pathname.split('/') || [];
118     const currentItemUuid = currentRoute[currentRoute.length - 1];
119     const currentResource = getResource(currentItemUuid)(state.resources);
120     const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(state.resources);
121     const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
122
123     return {
124         isAdmin: state.auth.user?.isAdmin,
125         currentResource,
126         frozenByFullName,
127     };
128 };
129
130 const mapDispatchToProps = (dispatch: Dispatch) => ({
131     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: any, isAdmin: boolean) => {
132         event.stopPropagation();
133         // When viewing the contents of a filter group, all contents should be treated as read only.
134         let readOnly = false;
135         if (resource.groupClass === 'filter') {
136             readOnly = true;
137         }
138         const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resource.uuid, readOnly));
139         if (menuKind === ContextMenuKind.ROOT_PROJECT) {
140             dispatch<any>(openUserContextMenu(event, resource as UserResource));
141         } else if (menuKind && resource) {
142             dispatch<any>(
143                 openContextMenu(event, {
144                     name: resource.name,
145                     uuid: resource.uuid,
146                     ownerUuid: resource.ownerUuid,
147                     isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
148                     kind: resource.kind,
149                     menuKind,
150                     isAdmin,
151                     isFrozen: !!resource.frozenByUuid,
152                     description: resource.description,
153                     storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
154                     properties: 'properties' in resource ? resource.properties : {},
155                 })
156             );
157         }
158     },
159 });
160
161 type DetailsCardProps = WithStyles<CssRules> & {
162     currentResource: ProjectResource | UserResource;
163     frozenByFullName?: string;
164     isAdmin: boolean;
165     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
166 };
167
168 type UserCardProps = WithStyles<CssRules> & {
169     currentResource: UserResource;
170     isAdmin: boolean;
171     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
172 };
173
174 type ProjectCardProps = WithStyles<CssRules> & {
175     currentResource: ProjectResource;
176     frozenByFullName: string | undefined;
177     isAdmin: boolean;
178     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
179 };
180
181 export const ProjectDetailsCard = connect(
182     mapStateToProps,
183     mapDispatchToProps
184 )(
185     withStyles(styles)((props: DetailsCardProps) => {
186         const { classes, currentResource, frozenByFullName, handleContextMenu, isAdmin } = props;
187         switch (currentResource.kind as string) {
188             case ResourceKind.USER:
189                 return (
190                     <UserCard
191                         classes={classes}
192                         currentResource={currentResource as UserResource}
193                         isAdmin={isAdmin}
194                         handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
195                     />
196                 );
197             case ResourceKind.PROJECT:
198                 return (
199                     <ProjectCard
200                         classes={classes}
201                         currentResource={currentResource as ProjectResource}
202                         frozenByFullName={frozenByFullName}
203                         isAdmin={isAdmin}
204                         handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
205                     />
206                 );
207             default:
208                 return null;
209         }
210     })
211 );
212
213 const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleContextMenu, isAdmin }) => {
214     const { fullName, uuid } = currentResource as UserResource & { fullName: string };
215
216     return (
217         <Card className={classes.root}>
218             <CardHeader
219                 className={classes.cardHeader}
220                 title={
221                     <section className={classes.nameContainer}>
222                         <Typography
223                             noWrap
224                             variant='h6'
225                         >
226                             {fullName}
227                         </Typography>
228                     </section>
229                 }
230                 action={
231                     <section className={classes.contextMenuSection}>
232                         {!currentResource.isActive && (
233                             <Typography>
234                                 <UserResourceAccountStatus uuid={uuid} />
235                             </Typography>
236                         )}
237                         <Tooltip
238                             title='More options'
239                             disableFocusListener
240                         >
241                             <IconButton
242                                 aria-label='More options'
243                                 onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
244                             >
245                                 <MoreVerticalIcon />
246                             </IconButton>
247                         </Tooltip>
248                     </section>
249                 }
250             />
251         </Card>
252     );
253 };
254
255 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleContextMenu, isAdmin }) => {
256     const { name, description } = currentResource as ProjectResource;
257     const [showDescription, setShowDescription] = React.useState(false);
258
259     const toggleDescription = () => {
260         setShowDescription(!showDescription);
261     };
262
263     return (
264         <Card className={classes.root}>
265             <CardHeader
266                 className={classes.cardHeader}
267                 title={
268                     <section className={classes.namePlate}>
269                         <Typography
270                             noWrap
271                             variant='h6'
272                             style={{ marginRight: '1rem' }}
273                         >
274                             {name}
275                         </Typography>
276                         <FavoriteStar
277                             className={classes.faveIcon}
278                             resourceUuid={currentResource.uuid}
279                         />
280                         <PublicFavoriteStar
281                             className={classes.faveIcon}
282                             resourceUuid={currentResource.uuid}
283                         />
284                         {!!frozenByFullName && (
285                             <Tooltip
286                                 className={classes.frozenIcon}
287                                 title={<span>Project was frozen by {frozenByFullName}</span>}
288                             >
289                                 <FreezeIcon style={{ fontSize: 'inherit' }} />
290                             </Tooltip>
291                         )}
292                     </section>
293                 }
294                 action={
295                     <section className={classes.contextMenuSection}>
296                         <Tooltip
297                             title='More options'
298                             disableFocusListener
299                         >
300                             <IconButton
301                                 aria-label='More options'
302                                 onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
303                             >
304                                 <MoreVerticalIcon />
305                             </IconButton>
306                         </Tooltip>
307                     </section>
308                 }
309             />
310             <CardContent className={classes.cardContent}>
311                 <section className={classes.subHeader}>
312                     <section className={classes.chipSection}>
313                         <Typography component='div'>
314                             {typeof currentResource.properties === 'object' &&
315                                 Object.keys(currentResource.properties).map((k) =>
316                                     Array.isArray(currentResource.properties[k])
317                                         ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
318                                         : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
319                                 )}
320                         </Typography>
321                     </section>
322                     <section className={classes.descriptionLabel}>
323                         {description ? (
324                             <Typography
325                                 className={classes.showMore}
326                                 onClick={toggleDescription}
327                             >
328                                 {!showDescription ? "Show full description" : "Hide full description"}
329                             </Typography>
330                         ) : (
331                             <Typography className={classes.noDescription}>no description available</Typography>
332                         )}
333                     </section>
334                 </section>
335                 <Collapse in={showDescription} timeout='auto'>
336                     <section>
337                         <Typography className={classes.description}>
338                             {description}
339                         </Typography>
340                     </section>
341                 </Collapse>
342             </CardContent>
343         </Card>
344     );
345 };