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