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 } 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 { MoreVerticalIcon, FreezeIcon } from 'components/icon/icon';
18 import { Resource } from 'models/resource';
19 import { IconButton } from '@material-ui/core';
20 import { ContextMenuResource, openUserContextMenu } from 'store/context-menu/context-menu-actions';
21 import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
22 import { CollectionResource } from 'models/collection';
23 import { ContextMenuKind } from 'views-components/context-menu/context-menu';
24 import { Dispatch } from 'redux';
25 import classNames from 'classnames';
26 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
41 | 'contextMenuSection'
46 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
52 border: '2px solid transparent',
55 border: '2px solid #ccc',
58 color: theme.palette.primary.main,
62 color: theme.palette.grey['600'],
75 marginRight: '0.8rem',
79 flexDirection: 'column',
80 transition: 'height 0.3s ease',
85 justifyContent: 'space-between',
94 margin: 'auto 0 0.5rem 0.3rem',
95 color: theme.palette.text.primary,
100 marginTop: '0.57rem',
102 color: theme.palette.text.primary,
104 contextMenuSection: {
106 flexDirection: 'row',
107 alignItems: 'center',
123 const mapStateToProps = (state: RootState) => {
124 const currentRoute = state.router.location?.pathname.split('/') || [];
125 const currentItemUuid = currentRoute[currentRoute.length - 1];
126 const currentResource = getResource(currentItemUuid)(state.resources);
127 const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(state.resources);
128 const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
129 const isSelected = currentItemUuid === state.detailsPanel.resourceUuid && state.detailsPanel.isOpened === true;
132 isAdmin: state.auth.user?.isAdmin,
139 const mapDispatchToProps = (dispatch: Dispatch) => ({
140 handleCardClick: (uuid: string) => {
141 dispatch<any>(loadDetailsPanel(uuid));
143 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: any, isAdmin: boolean) => {
144 event.stopPropagation();
145 // When viewing the contents of a filter group, all contents should be treated as read only.
146 let readOnly = false;
147 if (resource.groupClass === 'filter') {
150 const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resource.uuid, readOnly));
151 if (menuKind === ContextMenuKind.ROOT_PROJECT) {
152 dispatch<any>(openUserContextMenu(event, resource as UserResource));
153 } else if (menuKind && resource) {
155 openContextMenu(event, {
158 ownerUuid: resource.ownerUuid,
159 isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
163 isFrozen: !!resource.frozenByUuid,
164 description: resource.description,
165 storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
166 properties: 'properties' in resource ? resource.properties : {},
173 type DetailsCardProps = WithStyles<CssRules> & {
174 currentResource: ProjectResource | UserResource;
175 frozenByFullName?: string;
178 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
179 handleCardClick: (resource: any) => void;
182 type UserCardProps = WithStyles<CssRules> & {
183 currentResource: UserResource;
186 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
187 handleCardClick: (resource: any) => void;
190 type ProjectCardProps = WithStyles<CssRules> & {
191 currentResource: ProjectResource;
192 frozenByFullName: string | undefined;
195 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
196 handleCardClick: (resource: any) => void;
199 export const ProjectDetailsCard = connect(
203 withStyles(styles)((props: DetailsCardProps) => {
204 const { classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected } = props;
205 if (!currentResource) {
208 switch (currentResource.kind as string) {
209 case ResourceKind.USER:
213 currentResource={currentResource as UserResource}
215 isSelected={isSelected}
216 handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
217 handleCardClick={handleCardClick}
220 case ResourceKind.PROJECT:
224 currentResource={currentResource as ProjectResource}
225 frozenByFullName={frozenByFullName}
227 isSelected={isSelected}
228 handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
229 handleCardClick={handleCardClick}
238 const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
239 const { fullName, uuid } = currentResource as UserResource & { fullName: string };
242 <Card className={classNames(classes.root, isSelected ? classes.selected : '')} onClick={()=>handleCardClick(uuid)}>
244 className={classes.cardHeader}
246 <section className={classes.nameContainer}>
256 <section className={classes.contextMenuSection}>
257 {!currentResource.isActive && (
259 <UserResourceAccountStatus uuid={uuid} />
267 aria-label='More options'
268 onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
280 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
281 const { name, description, uuid } = currentResource as ProjectResource;
282 const [showDescription, setShowDescription] = React.useState(false);
284 const toggleDescription = () => {
285 setShowDescription(!showDescription);
289 <Card className={classNames(classes.root, isSelected ? classes.selected : '')} onClick={()=>handleCardClick(uuid)}>
291 className={classes.cardHeader}
293 <section className={classes.namePlate}>
297 style={{ marginRight: '1rem' }}
302 className={classes.faveIcon}
303 resourceUuid={currentResource.uuid}
306 className={classes.faveIcon}
307 resourceUuid={currentResource.uuid}
309 {!!frozenByFullName && (
311 className={classes.frozenIcon}
312 title={<span>Project was frozen by {frozenByFullName}</span>}
314 <FreezeIcon style={{ fontSize: 'inherit' }} />
320 <section className={classes.contextMenuSection}>
326 aria-label='More options'
327 onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
335 <CardContent className={classes.cardContent}>
336 <section className={classes.subHeader}>
337 <section className={classes.chipSection}>
338 <Typography component='div'>
339 {typeof currentResource.properties === 'object' &&
340 Object.keys(currentResource.properties).map((k) =>
341 Array.isArray(currentResource.properties[k])
342 ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
343 : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
347 <section className={classes.descriptionLabel} onClick={(ev)=>ev.stopPropagation()}>
350 className={classes.showMore}
351 onClick={toggleDescription}
353 {!showDescription ? "Show full description" : "Hide full description"}
356 <Typography className={classes.noDescription}>no description available</Typography>
360 <Collapse in={showDescription} timeout='auto'>
361 <section onClick={(ev)=>ev.stopPropagation()}>
362 <Typography className={classes.description}>