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