22235: adjusted toolbar styles
[arvados.git] / services / workbench2 / src / views-components / 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 { CustomStyleRulesCallback } from 'common/custom-theme';
7 import { Card, CardHeader, Typography, CardContent, Tooltip, Collapse, Grid } from '@mui/material';
8 import { WithStyles } from '@mui/styles';
9 import withStyles from '@mui/styles/withStyles';
10 import { ArvadosTheme } from 'common/custom-theme';
11 import { RootState } from 'store/store';
12 import { connect } from 'react-redux';
13 import { getResource } from 'store/resources/resources';
14 import { getPropertyChip } from '../resource-properties-form/property-chip';
15 import { ProjectResource } from 'models/project';
16 import { FavoriteStar, PublicFavoriteStar } from 'views-components/favorite-star/favorite-star';
17 import { FreezeIcon } from 'components/icon/icon';
18 import { Resource } from 'models/resource';
19 import { Dispatch } from 'redux';
20 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
21 import { ExpandChevronRight } from 'components/expand-chevron-right/expand-chevron-right';
22 import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar';
23 import { setSelectedResourceUuid } from 'store/selected-resource/selected-resource-actions';
24 import { deselectAllOthers } from 'store/multiselect/multiselect-actions';
25
26 type CssRules =
27     | 'root'
28     | 'cardHeaderContainer'
29     | 'cardHeader'
30     | 'descriptionToggle'
31     | 'noDescription'
32     | 'userNameContainer'
33     | 'cardContent'
34     | 'nameSection'
35     | 'namePlate'
36     | 'faveIcon'
37     | 'frozenIcon'
38     | 'chipSection'
39     | 'tag'
40     | 'description'
41     | 'toolbarStyles';
42
43 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
44     root: {
45         width: '100%',
46         marginBottom: '1rem',
47         flex: '0 0 auto',
48         padding: 0,
49         minHeight: '3rem',
50     },
51     noDescription: {
52         color: theme.palette.grey['600'],
53         fontStyle: 'italic',
54         marginLeft: '2rem',
55     },
56     userNameContainer: {
57         display: 'flex',
58         alignItems: 'center',
59         minHeight: '2.7rem',
60     },
61     cardHeaderContainer: {
62         width: '100%',
63         display: 'flex',
64         flexDirection: 'row',
65         alignItems: 'center',
66         justifyContent: 'space-between',
67     },
68     cardHeader: {
69         minWidth: '30rem',
70         padding: '0.2rem',
71     },
72     descriptionToggle: {
73         marginLeft: '-16px',
74     },
75     cardContent: {
76         display: 'flex',
77         flexDirection: 'column',
78         marginTop: '.5rem',
79         paddingTop: '0px',
80         paddingBottom: '0px',
81         paddingLeft: '.5rem',
82         paddingRight: '.5rem',
83     },
84     nameSection: {
85         display: 'flex',
86         flexDirection: 'row',
87         alignItems: 'center',
88     },
89     namePlate: {
90         display: 'flex',
91         flexDirection: 'row',
92         alignItems: 'center',
93         margin: 0,
94         minHeight: '2.7rem',
95         marginLeft: '.5rem',
96     },
97     faveIcon: {
98         fontSize: '0.8rem',
99         margin: 'auto 0 1rem 0.3rem',
100         color: theme.palette.text.primary,
101     },
102     frozenIcon: {
103         fontSize: '0.5rem',
104         marginLeft: '0.3rem',
105         height: '1rem',
106         color: theme.palette.text.primary,
107     },
108     chipSection: {
109         marginBottom: '.5rem',
110     },
111     tag: {
112         marginRight: '0.75rem',
113         marginBottom: '0.5rem',
114     },
115     description: {
116         marginTop: 0,
117         marginRight: '2rem',
118         marginLeft: '8px',
119         maxWidth: "50em",
120     },
121     toolbarStyles: {
122         marginRight: '-0.5rem',
123         paddingTop: '4px',
124     },
125 });
126
127 const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => {
128     const currentResource = getResource(properties.currentRouteUuid)(resources);
129     const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
130     const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
131     const isSelected = selectedResourceUuid === properties.currentRouteUuid;
132
133     return {
134         isAdmin: auth.user?.isAdmin,
135         currentResource,
136         frozenByFullName,
137         isSelected,
138     };
139 };
140
141 const mapDispatchToProps = (dispatch: Dispatch) => ({
142     handleCardClick: (uuid: string) => {
143         dispatch<any>(loadDetailsPanel(uuid));
144         dispatch<any>(setSelectedResourceUuid(uuid));
145         dispatch<any>(deselectAllOthers(uuid));
146     },
147 });
148
149 type ProjectCardProps = WithStyles<CssRules> & {
150     currentResource: ProjectResource;
151     frozenByFullName: string | undefined;
152     isAdmin: boolean;
153     isSelected: boolean;
154     handleCardClick: (resource: any) => void;
155 };
156
157 export const ProjectCard = connect(
158     mapStateToProps,
159     mapDispatchToProps
160 )(
161     withStyles(styles)((props: ProjectCardProps) => {
162         const { classes, currentResource, frozenByFullName, handleCardClick, isSelected } = props;
163         const { name, description, uuid } = currentResource as ProjectResource;
164         const [showDescription, setShowDescription] = React.useState(false);
165
166         const toggleDescription = () => {
167             setShowDescription(!showDescription);
168         };
169
170         const hasDescription = !!(description && description.length > 0);
171         const hasProperties = (typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0);
172         const expandable = hasDescription || hasProperties;
173
174         return (
175             <Card
176                 className={classes.root}
177                 onClick={() => handleCardClick(uuid)}
178                 data-cy='project-details-card'
179             >
180                 <Grid
181                     container
182                     wrap='nowrap'
183                     className={classes.cardHeaderContainer}
184                 >
185                     <CardHeader
186                         className={classes.cardHeader}
187                         title={
188                             <section className={classes.nameSection}>
189                                 <section className={classes.namePlate}>
190                                     <Typography
191                                         variant='h6'
192                                         style={{ marginRight: '1rem' }}
193                                     >
194                                                  {name}
195                                                  {expandable && <span className={classes.descriptionToggle}
196                                                                       onClick={toggleDescription}
197                                                                       data-cy="toggle-description">
198                                                      <ExpandChevronRight expanded={showDescription} />
199                                                  </span>}
200                                     </Typography>
201
202                                     <FavoriteStar
203                                         className={classes.faveIcon}
204                                         resourceUuid={currentResource.uuid}
205                                     />
206                                     <PublicFavoriteStar
207                                         className={classes.faveIcon}
208                                         resourceUuid={currentResource.uuid}
209                                     />
210                                                   {!!frozenByFullName && (
211                                                       <Tooltip
212                                                           className={classes.frozenIcon}
213                                                           disableFocusListener
214                                                           title={<span>Project was frozen by {frozenByFullName}</span>}
215                                                       >
216                                                           <FreezeIcon style={{ fontSize: 'inherit' }} />
217                                                       </Tooltip>
218                                                   )}
219                                                                             {!hasDescription && (
220                                                                                 <Typography
221                                                                                     data-cy='no-description'
222                                                                                     className={classes.noDescription}
223                                                                                 >
224                                                                                              no description available
225                                                                                 </Typography>
226                                                                             )}
227
228                                 </section>
229                             </section>
230                         }
231                     />
232                     {isSelected && <MultiselectToolbar injectedStyles={classes.toolbarStyles} />}
233                 </Grid>
234
235                 {expandable && <Collapse
236                                    in={showDescription}
237                                    timeout='auto'
238                                    collapsedSize='0rem'
239                                >
240                     <CardContent className={classes.cardContent}>
241                         {hasProperties &&
242                          <section data-cy='project-properties'>
243                              <Typography
244                                  component='div'
245                                  className={classes.chipSection}
246                              >
247                                  {Object.keys(currentResource.properties).map((k) =>
248                                      Array.isArray(currentResource.properties[k])
249                                      ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
250                                      : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
251                                  )}
252                              </Typography>
253                          </section>}
254
255                         {hasDescription && (
256                             <section data-cy='project-description'>
257                                 <Typography
258                                     className={classes.description}
259                                     component='div'
260                                     //dangerouslySetInnerHTML is ok here only if description is sanitized,
261                                     //which it is before it is loaded into the redux store
262                                     dangerouslySetInnerHTML={{ __html: description }}
263                                 />
264                             </section>
265                         )}
266                     </CardContent>
267                 </Collapse>}
268             </Card>
269         );
270     })
271 );