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'
39 | 'accountStatusSection'
44 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
56 color: theme.palette.grey['600'],
58 padding: '0 0 0.5rem 1rem',
66 cardHeaderContainer: {
70 justifyContent: 'space-between',
74 padding: '0.2rem 0.4rem 0.2rem 1rem',
80 paddingBottom: '0.5rem',
84 flexDirection: 'column',
86 paddingLeft: '0.1rem',
102 margin: 'auto 0 1rem 0.3rem',
103 color: theme.palette.text.primary,
107 marginLeft: '0.3rem',
109 color: theme.palette.text.primary,
111 accountStatusSection: {
113 flexDirection: 'row',
114 alignItems: 'center',
118 marginBottom: '-2rem',
121 marginRight: '0.75rem',
122 marginBottom: '0.5rem',
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));
153 type DetailsCardProps = WithStyles<CssRules> & {
154 currentResource: ProjectResource | UserResource;
155 frozenByFullName?: string;
158 handleCardClick: (resource: any) => void;
161 type UserCardProps = WithStyles<CssRules> & {
162 currentResource: UserResource;
165 handleCardClick: (resource: any) => void;
168 type ProjectCardProps = WithStyles<CssRules> & {
169 currentResource: ProjectResource;
170 frozenByFullName: string | undefined;
173 handleCardClick: (resource: any) => void;
176 export const ProjectDetailsCard = connect(
180 withStyles(styles)((props: DetailsCardProps) => {
181 const { classes, currentResource, frozenByFullName, handleCardClick, isAdmin, isSelected } = props;
182 if (!currentResource) {
185 switch (currentResource.kind as string) {
186 case ResourceKind.USER:
190 currentResource={currentResource as UserResource}
192 isSelected={isSelected}
193 handleCardClick={handleCardClick}
196 case ResourceKind.PROJECT:
200 currentResource={currentResource as ProjectResource}
201 frozenByFullName={frozenByFullName}
203 isSelected={isSelected}
204 handleCardClick={handleCardClick}
213 const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleCardClick, isSelected }) => {
214 const { fullName, uuid } = currentResource as UserResource & { fullName: string };
218 className={classes.root}
219 onClick={() => handleCardClick(uuid)}
220 data-cy='user-details-card'
225 className={classes.cardHeaderContainer}
228 className={classes.cardHeader}
230 <section className={classes.userNameContainer}>
237 <section className={classes.accountStatusSection}>
238 {!currentResource.isActive && (
240 <UserResourceAccountStatus uuid={uuid} />
247 {isSelected && <MultiselectToolbar />}
253 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleCardClick, isSelected }) => {
254 const { name, description, uuid } = currentResource as ProjectResource;
255 const [showDescription, setShowDescription] = React.useState(false);
256 const [showProperties, setShowProperties] = React.useState(false);
258 const toggleDescription = () => {
259 setShowDescription(!showDescription);
262 const toggleProperties = () => {
263 setShowProperties(!showProperties);
268 className={classes.root}
269 onClick={() => handleCardClick(uuid)}
270 data-cy='project-details-card'
275 className={classes.cardHeaderContainer}
278 className={classes.cardHeader}
280 <section className={classes.nameSection}>
281 <section className={classes.namePlate}>
284 style={{ marginRight: '1rem' }}
289 className={classes.faveIcon}
290 resourceUuid={currentResource.uuid}
293 className={classes.faveIcon}
294 resourceUuid={currentResource.uuid}
296 {!!frozenByFullName && (
298 className={classes.frozenIcon}
300 title={<span>Project was frozen by {frozenByFullName}</span>}
302 <FreezeIcon style={{ fontSize: 'inherit' }} />
309 {isSelected && <MultiselectToolbar />}
311 <section onClick={(ev) => ev.stopPropagation()}>
314 onClick={toggleDescription}
315 className={classes.descriptionToggle}
317 <ExpandChevronRight expanded={showDescription} />
318 <section className={classes.showMore}>
322 collapsedHeight='1.25rem'
325 className={classes.description}
326 data-cy='project-description'
327 //dangerouslySetInnerHTML is ok here only if description is sanitized,
328 //which it is before it is loaded into the redux store
329 dangerouslySetInnerHTML={{ __html: description }}
336 className={classes.noDescription}
337 data-cy='no-description'
339 no description available
342 {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
344 onClick={toggleProperties}
345 className={classes.descriptionToggle}
347 <ExpandChevronRight expanded={showProperties} />
348 <section className={classes.showMore}>
352 collapsedHeight='35px'
355 className={classes.description}
356 data-cy='project-description'
358 <CardContent className={classes.cardContent}>
359 <Typography component='div' className={classes.chipSection}>
360 {Object.keys(currentResource.properties).map((k) =>
361 Array.isArray(currentResource.properties[k])
362 ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
363 : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)