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