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 { ResourceKind } from 'models/resource';
14 import { UserResource } from 'models/user';
15 import { UserResourceAccountStatus } from 'views-components/data-explorer/renderers';
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';
28 | 'cardHeaderContainer'
40 | 'accountStatusSection'
46 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
58 color: theme.palette.grey['600'],
67 cardHeaderContainer: {
72 justifyContent: 'space-between',
76 padding: '0.2rem 0.4rem 0.2rem 1rem',
79 //shows only the first 3 buttons
80 width: '12rem !important',
86 paddingBottom: '0.5rem',
90 flexDirection: 'column',
92 paddingLeft: '0.1rem',
101 flexDirection: 'row',
102 alignItems: 'center',
108 margin: 'auto 0 1rem 0.3rem',
109 color: theme.palette.text.primary,
113 marginLeft: '0.3rem',
115 color: theme.palette.text.primary,
117 accountStatusSection: {
119 flexDirection: 'row',
120 alignItems: 'center',
125 alignItems: 'center',
129 marginBottom: '-2rem',
132 marginRight: '0.75rem',
133 marginBottom: '0.5rem',
141 const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => {
142 const currentResource = getResource(properties.currentRouteUuid)(resources);
143 const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
144 const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
145 const isSelected = selectedResourceUuid === properties.currentRouteUuid;
148 isAdmin: auth.user?.isAdmin,
155 const mapDispatchToProps = (dispatch: Dispatch) => ({
156 handleCardClick: (uuid: string) => {
157 dispatch<any>(loadDetailsPanel(uuid));
158 dispatch<any>(setSelectedResourceUuid(uuid));
159 dispatch<any>(deselectAllOthers(uuid));
164 type DetailsCardProps = WithStyles<CssRules> & {
165 currentResource: ProjectResource | UserResource;
166 frozenByFullName?: string;
169 handleCardClick: (resource: any) => void;
172 type UserCardProps = WithStyles<CssRules> & {
173 currentResource: UserResource;
176 handleCardClick: (resource: any) => void;
179 type ProjectCardProps = WithStyles<CssRules> & {
180 currentResource: ProjectResource;
181 frozenByFullName: string | undefined;
184 handleCardClick: (resource: any) => void;
187 export const ProjectDetailsCard = connect(
191 withStyles(styles)((props: DetailsCardProps) => {
192 const { classes, currentResource, frozenByFullName, handleCardClick, isAdmin, isSelected } = props;
193 if (!currentResource) {
196 switch (currentResource.kind as string) {
197 case ResourceKind.USER:
201 currentResource={currentResource as UserResource}
203 isSelected={isSelected}
204 handleCardClick={handleCardClick}
207 case ResourceKind.PROJECT:
211 currentResource={currentResource as ProjectResource}
212 frozenByFullName={frozenByFullName}
214 isSelected={isSelected}
215 handleCardClick={handleCardClick}
224 const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleCardClick, isSelected }) => {
225 const { fullName, uuid } = currentResource as UserResource & { fullName: string };
229 className={classes.root}
230 onClick={() => handleCardClick(uuid)}
231 data-cy='user-details-card'
236 className={classes.cardHeaderContainer}
239 className={classes.cardHeader}
241 <section className={classes.userNameContainer}>
248 <section className={classes.accountStatusSection}>
249 {!currentResource.isActive && (
251 <UserResourceAccountStatus uuid={uuid} />
258 {isSelected && <MultiselectToolbar />}
264 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleCardClick, isSelected }) => {
265 const { name, description, uuid } = currentResource as ProjectResource;
266 const [showDescription, setShowDescription] = React.useState(false);
267 const [showProperties, setShowProperties] = React.useState(false);
269 const toggleDescription = () => {
270 setShowDescription(!showDescription);
273 const toggleProperties = () => {
274 setShowProperties(!showProperties);
279 className={classes.root}
280 onClick={() => handleCardClick(uuid)}
281 data-cy='project-details-card'
286 className={classes.cardHeaderContainer}
289 className={classes.cardHeader}
291 <section className={classes.nameSection}>
292 <section className={classes.namePlate}>
295 style={{ marginRight: '1rem' }}
300 className={classes.faveIcon}
301 resourceUuid={currentResource.uuid}
304 className={classes.faveIcon}
305 resourceUuid={currentResource.uuid}
307 {!!frozenByFullName && (
309 className={classes.frozenIcon}
311 title={<span>Project was frozen by {frozenByFullName}</span>}
313 <FreezeIcon style={{ fontSize: 'inherit' }} />
317 {!description && <Typography className={classes.noDescription}>no description available</Typography>}
321 {isSelected && <MultiselectToolbar injectedStyles={classes.projectToolbar} />}
323 <section onClick={(ev) => ev.stopPropagation()}>
326 onClick={toggleDescription}
327 className={classes.descriptionToggle}
329 <ExpandChevronRight expanded={showDescription} />
330 <section className={classes.showMore}>
334 collapsedHeight='1.25rem'
337 className={classes.description}
338 data-cy='project-description'
339 //dangerouslySetInnerHTML is ok here only if description is sanitized,
340 //which it is before it is loaded into the redux store
341 dangerouslySetInnerHTML={{ __html: description }}
349 {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
351 onClick={toggleProperties}
352 className={classes.descriptionToggle}
354 <div className={classes.chipToggle}>
355 <ExpandChevronRight expanded={showProperties} />
357 <section className={classes.showMore}>
361 collapsedHeight='35px'
364 className={classes.description}
365 data-cy='project-description'
367 <CardContent className={classes.cardContent}>
368 <Typography component='div' className={classes.chipSection}>
369 {Object.keys(currentResource.properties).map((k) =>
370 Array.isArray(currentResource.properties[k])
371 ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
372 : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)