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 } 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         paddingBottom: '-1rem',
120     },
121     tag: {
122         marginRight: '1rem',
123         marginTop: '1rem',
124     },
125     description: {
126         maxWidth: '90%',
127         marginTop: 0,
128     },
129 });
130
131 const mapStateToProps = ({auth, selectedResourceUuid, resources, properties}: RootState) => {
132     const currentResource = getResource(properties.currentRouteUuid)(resources);
133     const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
134     const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
135     const isSelected = selectedResourceUuid === properties.currentRouteUuid;
136
137     return {
138         isAdmin: auth.user?.isAdmin,
139         currentResource,
140         frozenByFullName,
141         isSelected,
142     };
143 };
144
145 const mapDispatchToProps = (dispatch: Dispatch) => ({
146     handleCardClick: (uuid: string) => {
147         dispatch<any>(loadDetailsPanel(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, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
244     const { fullName, uuid } = currentResource as UserResource & { fullName: string };
245
246     return (
247         <Card
248             className={classNames(classes.root, isSelected ? classes.selected : '')}
249             onClick={() => handleCardClick(uuid)}
250             data-cy='user-details-card'
251         >
252             <CardHeader
253                 className={classes.cardHeader}
254                 title={
255                     <section className={classes.userNameContainer}>
256                         <Typography
257                             noWrap
258                             variant='h6'
259                         >
260                             {fullName}
261                         </Typography>
262                     </section>
263                 }
264                 action={
265                     <section className={classes.contextMenuSection}>
266                         {!currentResource.isActive && (
267                             <Typography>
268                                 <UserResourceAccountStatus uuid={uuid} />
269                             </Typography>
270                         )}
271                         <Tooltip
272                             title='More options'
273                             disableFocusListener
274                         >
275                             <IconButton
276                                 aria-label='More options'
277                                 data-cy='kebab-icon'
278                                 onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
279                             >
280                                 <MoreVerticalIcon />
281                             </IconButton>
282                         </Tooltip>
283                     </section>
284                 }
285             />
286         </Card>
287     );
288 };
289
290 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleCardClick, isSelected }) => {
291     const { name, description, uuid } = currentResource as ProjectResource;
292     const [showDescription, setShowDescription] = React.useState(false);
293     const [showProperties, setShowProperties] = React.useState(false);
294
295     const toggleDescription = () => {
296         setShowDescription(!showDescription);
297     };
298
299     const toggleProperties = () => {
300         setShowProperties(!showProperties);
301     };
302
303     return (
304         <Card
305             className={classNames(classes.root, isSelected ? classes.selected : '')}
306             onClick={() => handleCardClick(uuid)}
307             data-cy='project-details-card'
308         >
309             <CardHeader
310                 className={classes.cardHeader}
311                 title={
312                     <section className={classes.nameSection}>
313                         <section className={classes.namePlate}>
314                             <Typography
315                                 noWrap
316                                 variant='h6'
317                                 style={{ marginRight: '1rem' }}
318                             >
319                                 {name}
320                             </Typography>
321                             <FavoriteStar
322                                 className={classes.faveIcon}
323                                 resourceUuid={currentResource.uuid}
324                             />
325                             <PublicFavoriteStar
326                                 className={classes.faveIcon}
327                                 resourceUuid={currentResource.uuid}
328                             />
329                             {!!frozenByFullName && (
330                                 <Tooltip
331                                     className={classes.frozenIcon}
332                                     disableFocusListener
333                                     title={<span>Project was frozen by {frozenByFullName}</span>}
334                                 >
335                                     <FreezeIcon style={{ fontSize: 'inherit' }} />
336                                 </Tooltip>
337                             )}
338                         </section>
339                     </section>
340                 }
341                 action={
342                     <section className={classes.toolbarSection}>
343                         {isSelected && <MultiselectToolbar />}
344                     </section>
345                 }
346             />
347             <section onClick={(ev) => ev.stopPropagation()}>
348                 {description ? (
349                     <section
350                         onClick={toggleDescription}
351                         className={classes.descriptionToggle}
352                     >
353                         <ExpandChevronRight expanded={showDescription} />
354                         <section className={classes.showMore}>
355                             <Collapse
356                                 in={showDescription}
357                                 timeout='auto'
358                                 collapsedHeight='1.25rem'
359                             >
360                                 <Typography
361                                     className={classes.description}
362                                     data-cy='project-description'
363                                 >
364                                     {description}
365                                 </Typography>
366                             </Collapse>
367                         </section>
368                     </section>
369                 ) : (
370                     <Typography
371                         className={classes.noDescription}
372                         data-cy='no-description'
373                     >
374                         no description available
375                     </Typography>
376                 )}
377                 {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
378                     <section
379                         onClick={toggleProperties}
380                         className={classes.descriptionToggle}
381                     >
382                         <ExpandChevronRight expanded={showProperties} />
383                         <section className={classes.showMore}>
384                             <Collapse
385                                 in={showProperties}
386                                 timeout='auto'
387                                 collapsedHeight='35px'
388                             >
389                                 <div
390                                     className={classes.description}
391                                     data-cy='project-description'
392                                 >
393                                     <CardContent className={classes.cardContent}>
394                                         <Typography component='div'>
395                                             {Object.keys(currentResource.properties).map((k) =>
396                                                 Array.isArray(currentResource.properties[k])
397                                                     ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
398                                                     : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
399                                             )}
400                                         </Typography>
401                                     </CardContent>
402                                 </div>
403                             </Collapse>
404                         </section>
405                     </section>
406                 ) : null}
407             </section>
408         </Card>
409     );
410 };