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 { Card, CardHeader, WithStyles, withStyles, Typography, CardContent, Tooltip } from '@material-ui/core';
7 import { StyleRulesCallback } from '@material-ui/core';
8 import { ArvadosTheme } from 'common/custom-theme';
9 import { RootState } from 'store/store';
10 import { connect } from 'react-redux';
11 import { getResource } from 'store/resources/resources';
12 import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar';
13 import { getPropertyChip } from '../resource-properties-form/property-chip';
14 import { ProjectResource } from 'models/project';
15 import { ResourceKind } from 'models/resource';
16 import { UserResource } from 'models/user';
17 import { UserResourceAccountStatus } from 'views-components/data-explorer/renderers';
18 import { FavoriteStar, PublicFavoriteStar } from 'views-components/favorite-star/favorite-star';
19 import { FreezeIcon } from 'components/icon/icon';
20 import { Resource } from 'models/resource';
21 import { MoreVerticalIcon } from 'components/icon/icon';
22 import { IconButton } from '@material-ui/core';
23 import { ContextMenuResource } from 'store/context-menu/context-menu-actions';
24 import { resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
25 import { openContextMenu } from 'store/context-menu/context-menu-actions'; 
26 import { CollectionResource } from 'models/collection';
27 import { RichTextEditorLink } from 'components/rich-text-editor-link/rich-text-editor-link';    
28
29 type CssRules =
30     | 'root'
31     | 'cardHeader'
32     | 'showMore'
33     | 'nameContainer'
34     | 'activeIndicator'
35     | 'cardContent'
36     | 'namePlate'
37     | 'faveIcon'
38     | 'frozenIcon'
39     | 'attributeSection'
40     | 'attribute'
41     | 'chipSection'
42     | 'tag';
43
44 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
45     root: {
46         width: '100%',
47         marginBottom: '1rem',
48         flex: '0 0 auto',
49     },
50     showMore: {
51         color: theme.palette.primary.main,
52         cursor: 'pointer',
53         maxWidth: '10rem',
54     },
55     nameContainer: {
56         display: 'flex',
57     },
58     activeIndicator: {
59         margin: '0.3rem auto auto 1rem',
60     },
61     cardHeader: {
62         paddingTop: '0.4rem',
63     },
64     cardContent: {
65         display: 'flex',
66         flexDirection: 'column',
67         marginTop: '-1rem',
68     },
69     namePlate: {
70         display: 'flex',
71         flexDirection: 'row',
72     },
73     faveIcon: {
74         fontSize: '0.8rem',
75         margin: 'auto 0 0.5rem 0.3rem',
76         color: theme.palette.text.primary,
77     },
78     frozenIcon: {
79         fontSize: '0.5rem',
80         marginLeft: '0.3rem',
81         marginTop: '0.57rem',
82         height: '1rem',
83         color: theme.palette.text.primary,
84     },
85     attributeSection: {
86         display: 'flex',
87         flexWrap: 'wrap',
88     },
89     attribute: {
90         marginBottom: '0.5rem',
91         marginRight: '1rem',
92         padding: '0.5rem',
93         borderRadius: '5px',
94     },
95     chipSection: {
96         display: 'flex',
97         flexWrap: 'wrap',
98     },
99     tag: {
100         marginRight: '1rem',
101         marginTop: '0.5rem',
102     },
103 });
104
105 const mapStateToProps = (state: RootState) => {
106     const currentRoute = state.router.location?.pathname.split('/') || [];
107     const currentItemUuid = currentRoute[currentRoute.length - 1];
108     const currentResource = getResource(currentItemUuid)(state.resources);
109     const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(state.resources);
110     const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
111
112     return {
113         isAdmin: state.auth.user?.isAdmin,
114         currentResource,
115         frozenByFullName,
116     };
117 };
118
119 const mapDispatchToProps = (dispatch: any) => ({
120     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: any, isAdmin: boolean) => {
121         // When viewing the contents of a filter group, all contents should be treated as read only.
122         let readOnly = false;
123         if (resource.groupClass === 'filter') {
124             readOnly = true;
125         }
126
127         const menuKind = dispatch(resourceUuidToContextMenuKind(resource.uuid, readOnly));
128         if (menuKind && resource) {
129             dispatch(
130                 openContextMenu(event, {
131                     name: resource.name,
132                     uuid: resource.uuid,
133                     ownerUuid: resource.ownerUuid,
134                     isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
135                     kind: resource.kind,
136                     menuKind,
137                     isAdmin,
138                     isFrozen: !!resource.frozenByUuid,
139                     description: resource.description,
140                     storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
141                     properties: 'properties' in resource ? resource.properties : {},
142                 })
143             );
144         }
145
146     },
147 });
148
149 type DetailsCardProps = WithStyles<CssRules> & {
150     currentResource: ProjectResource | UserResource;
151     frozenByFullName?: string;
152     isAdmin: boolean;
153     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
154 };
155
156 type UserCardProps = WithStyles<CssRules> & {
157     currentResource: UserResource;
158 };
159
160 type ProjectCardProps = WithStyles<CssRules> & {
161     currentResource: ProjectResource;
162     frozenByFullName: string | undefined;
163     isAdmin: boolean;
164     handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
165 };
166
167 export const ProjectDetailsCard = connect(
168     mapStateToProps,
169     mapDispatchToProps
170 )(
171     withStyles(styles)((props: DetailsCardProps) => {
172         const { classes, currentResource, frozenByFullName, handleContextMenu, isAdmin } = props;
173         switch (currentResource.kind as string) {
174             case ResourceKind.USER:
175                 return (
176                     <UserCard
177                         classes={classes}
178                         currentResource={currentResource as UserResource}
179                     />
180                 );
181             case ResourceKind.PROJECT:
182                 return (
183                     <ProjectCard
184                         classes={classes}
185                         currentResource={currentResource as ProjectResource}
186                         frozenByFullName={frozenByFullName}
187                         isAdmin={isAdmin}
188                         handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
189                     />
190                 );
191             default:
192                 return null;
193         }
194     })
195 );
196
197 const UserCard: React.FC<UserCardProps> = ({ classes, currentResource }) => {
198     const { fullName, uuid } = currentResource as UserResource & { fullName: string };
199
200     return (
201         <Card className={classes.root}>
202             <CardHeader
203                 className={classes.cardHeader}
204                 title={
205                     <section className={classes.nameContainer}>
206                         <Typography
207                             noWrap
208                             variant='h6'
209                         >
210                             {fullName}
211                         </Typography>
212                         {!currentResource.isActive && (
213                             <Typography className={classes.activeIndicator}>
214                                 <UserResourceAccountStatus uuid={uuid} />
215                             </Typography>
216                         )}
217                     </section>
218                 }
219                 action={<MultiselectToolbar inputSelectedUuid={uuid} />}
220             />
221         </Card>
222     );
223 };
224
225 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleContextMenu, isAdmin }) => {
226     const { name, description } = currentResource as ProjectResource;
227
228     return (
229         <Card className={classes.root}>
230             <CardHeader
231                 className={classes.cardHeader}
232                 title={
233                     <>
234                     <section className={classes.namePlate}>
235                         <Typography
236                             noWrap
237                             variant='h6'
238                             style={{ marginRight: '1rem' }}
239                         >
240                             {name}
241                         </Typography>
242                         <FavoriteStar
243                             className={classes.faveIcon}
244                             resourceUuid={currentResource.uuid}
245                         />
246                         <PublicFavoriteStar
247                             className={classes.faveIcon}
248                             resourceUuid={currentResource.uuid}
249                         />
250                         {!!frozenByFullName && <Tooltip
251                             className={classes.frozenIcon}
252                             title={<span>Project was frozen by {frozenByFullName}</span>}
253                         >
254                             <FreezeIcon style={{ fontSize: 'inherit' }} />
255                         </Tooltip>}
256                     </section>
257                     <section className={classes.chipSection}>
258                     <Typography component='div'>
259                         {typeof currentResource.properties === 'object' &&
260                             Object.keys(currentResource.properties).map((k) =>
261                                 Array.isArray(currentResource.properties[k])
262                                     ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
263                                     : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
264                             )}
265                     </Typography>
266                 </section>
267                     </>
268                 }
269                     
270                     
271                 action={<Tooltip
272                     title='More options'
273                     disableFocusListener
274                 >
275                     <IconButton
276                         aria-label='More options'
277                         onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
278                     >
279                         <MoreVerticalIcon />
280                     </IconButton>
281                 </Tooltip>}
282             />
283             <CardContent className={classes.cardContent}>
284                 {description && (
285                         <section>
286                             <div className={classes.showMore}>
287                                 <RichTextEditorLink
288                                     title={`Description of ${name}`}
289                                     content={description}
290                                     label='Show full description'
291                                 />
292                             </div>
293                         </section>
294                     )}
295             </CardContent>
296         </Card>
297     );
298 };