1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
6 import { StyleRulesCallback, Card, CardHeader, WithStyles, withStyles, Typography, CardContent, Tooltip, Collapse, Grid } 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 { FavoriteStar, PublicFavoriteStar } from 'views-components/favorite-star/favorite-star';
14 import { FreezeIcon } from 'components/icon/icon';
15 import { Resource } from 'models/resource';
16 import { Dispatch } from 'redux';
17 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
18 import { ExpandChevronRight } from 'components/expand-chevron-right/expand-chevron-right';
19 import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar';
20 import { setSelectedResourceUuid } from 'store/selected-resource/selected-resource-actions';
21 import { deselectAllOthers } from 'store/multiselect/multiselect-actions';
25 | 'cardHeaderContainer'
42 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
54 color: theme.palette.grey['600'],
63 cardHeaderContainer: {
68 justifyContent: 'space-between',
72 padding: '0.2rem 0.4rem 0.2rem 1rem',
78 paddingBottom: '0.5rem',
82 flexDirection: 'column',
84 paddingLeft: '0.1rem',
100 margin: 'auto 0 1rem 0.3rem',
101 color: theme.palette.text.primary,
105 marginLeft: '0.3rem',
107 color: theme.palette.text.primary,
111 alignItems: 'center',
115 marginBottom: '-2rem',
118 marginRight: '0.75rem',
119 marginBottom: '0.5rem',
126 marginRight: '-1rem',
130 const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => {
131 const currentResource = getResource(properties.currentRouteUuid)(resources);
132 const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
133 const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
134 const isSelected = selectedResourceUuid === properties.currentRouteUuid;
137 isAdmin: auth.user?.isAdmin,
144 const mapDispatchToProps = (dispatch: Dispatch) => ({
145 handleCardClick: (uuid: string) => {
146 dispatch<any>(loadDetailsPanel(uuid));
147 dispatch<any>(setSelectedResourceUuid(uuid));
148 dispatch<any>(deselectAllOthers(uuid));
152 type ProjectCardProps = WithStyles<CssRules> & {
153 currentResource: ProjectResource;
154 frozenByFullName: string | undefined;
157 handleCardClick: (resource: any) => void;
160 export const ProjectCard = connect(
164 withStyles(styles)((props: ProjectCardProps) => {
165 const { classes, currentResource, frozenByFullName, handleCardClick, isSelected } = props;
166 const { name, description, uuid } = currentResource as ProjectResource;
167 const [showDescription, setShowDescription] = React.useState(false);
168 const [showProperties, setShowProperties] = React.useState(false);
170 const toggleDescription = () => {
171 setShowDescription(!showDescription);
174 const toggleProperties = () => {
175 setShowProperties(!showProperties);
178 const parser = new DOMParser();
182 className={classes.root}
183 onClick={() => handleCardClick(uuid)}
184 data-cy='project-details-card'
189 className={classes.cardHeaderContainer}
192 className={classes.cardHeader}
194 <section className={classes.nameSection}>
195 <section className={classes.namePlate}>
198 style={{ marginRight: '1rem' }}
203 className={classes.faveIcon}
204 resourceUuid={currentResource.uuid}
207 className={classes.faveIcon}
208 resourceUuid={currentResource.uuid}
210 {!!frozenByFullName && (
212 className={classes.frozenIcon}
214 title={<span>Project was frozen by {frozenByFullName}</span>}
216 <FreezeIcon style={{ fontSize: 'inherit' }} />
222 data-cy='no-description'
223 className={classes.noDescription}
225 no description available
231 {isSelected && <MultiselectToolbar injectedStyles={classes.toolbarStyles} />}
233 <section onClick={(ev) => ev.stopPropagation()}>
236 onClick={toggleDescription}
237 className={classes.descriptionToggle}
238 data-cy='toggle-description'
240 <ExpandChevronRight expanded={showDescription} />
241 <section className={classes.showMore}>
245 collapsedHeight='1.25rem'
248 className={classes.description}
249 data-cy='project-description'
250 //dangerouslySetInnerHTML is ok here only if description is sanitized,
251 //which it is before it is loaded into the redux store
252 dangerouslySetInnerHTML={{ __html: parser.parseFromString(description, 'text/html').body.textContent || '' }}
260 {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
262 onClick={toggleProperties}
263 className={classes.descriptionToggle}
266 className={classes.chipToggle}
267 data-cy='toggle-chips'
269 <ExpandChevronRight expanded={showProperties} />
271 <section className={classes.showMore}>
275 collapsedHeight='35px'
278 className={classes.description}
279 data-cy='project-description'
281 <CardContent className={classes.cardContent}>
284 className={classes.chipSection}
286 {Object.keys(currentResource.properties).map((k) =>
287 Array.isArray(currentResource.properties[k])
288 ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
289 : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)