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 } 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';
27 import { ExpandChevronRight } from 'components/expand-chevron-right/expand-chevron-right';
42 | 'contextMenuSection'
46 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
52 border: '2px solid transparent',
55 border: '2px solid #ccc',
59 background: 'linear-gradient(to right, black, transparent)',
60 backgroundClip: 'text',
64 color: theme.palette.grey['600'],
71 padding: '0.2rem 0.4rem 0.1rem 1rem',
77 paddingBottom: '0.5rem',
81 flexDirection: 'column',
82 marginTop: '-1.75rem',
88 justifyContent: 'space-between',
95 paddingBottom: '0.5rem',
99 margin: 'auto 0 0.5rem 0.3rem',
100 color: theme.palette.text.primary,
104 marginLeft: '0.3rem',
107 color: theme.palette.text.primary,
109 contextMenuSection: {
111 flexDirection: 'row',
112 alignItems: 'center',
113 paddingTop: '0.25rem',
125 const mapStateToProps = (state: RootState) => {
126 const currentRoute = state.router.location?.pathname.split('/') || [];
127 const currentItemUuid = currentRoute[currentRoute.length - 1];
128 const currentResource = getResource(currentItemUuid)(state.resources);
129 const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(state.resources);
130 const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
131 const isSelected = currentItemUuid === state.detailsPanel.resourceUuid && state.detailsPanel.isOpened === true;
134 isAdmin: state.auth.user?.isAdmin,
141 const mapDispatchToProps = (dispatch: Dispatch) => ({
142 handleCardClick: (uuid: string) => {
143 dispatch<any>(loadDetailsPanel(uuid));
145 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: any, isAdmin: boolean) => {
146 event.stopPropagation();
147 // When viewing the contents of a filter group, all contents should be treated as read only.
148 let readOnly = false;
149 if (resource.groupClass === 'filter') {
152 let menuKind = dispatch<any>(resourceUuidToContextMenuKind(resource.uuid, readOnly));
153 if (menuKind === ContextMenuKind.ROOT_PROJECT) {
154 menuKind = ContextMenuKind.USER_DETAILS;
156 if (menuKind && resource) {
158 openContextMenu(event, {
161 ownerUuid: resource.ownerUuid,
162 isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
166 isFrozen: !!resource.frozenByUuid,
167 description: resource.description,
168 storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
169 properties: 'properties' in resource ? resource.properties : {},
176 type DetailsCardProps = WithStyles<CssRules> & {
177 currentResource: ProjectResource | UserResource;
178 frozenByFullName?: string;
181 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
182 handleCardClick: (resource: any) => void;
185 type UserCardProps = WithStyles<CssRules> & {
186 currentResource: UserResource;
189 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
190 handleCardClick: (resource: any) => void;
193 type ProjectCardProps = WithStyles<CssRules> & {
194 currentResource: ProjectResource;
195 frozenByFullName: string | undefined;
198 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
199 handleCardClick: (resource: any) => void;
202 export const ProjectDetailsCard = connect(
206 withStyles(styles)((props: DetailsCardProps) => {
207 const { classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected } = props;
208 if (!currentResource) {
211 switch (currentResource.kind as string) {
212 case ResourceKind.USER:
216 currentResource={currentResource as UserResource}
218 isSelected={isSelected}
219 handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
220 handleCardClick={handleCardClick}
223 case ResourceKind.PROJECT:
227 currentResource={currentResource as ProjectResource}
228 frozenByFullName={frozenByFullName}
230 isSelected={isSelected}
231 handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
232 handleCardClick={handleCardClick}
241 const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
242 const { fullName, uuid } = currentResource as UserResource & { fullName: string };
246 className={classNames(classes.root, isSelected ? classes.selected : '')}
247 onClick={() => handleCardClick(uuid)}
248 data-cy='user-details-card'
251 className={classes.cardHeader}
253 <section className={classes.userNameContainer}>
263 <section className={classes.contextMenuSection}>
264 {!currentResource.isActive && (
266 <UserResourceAccountStatus uuid={uuid} />
274 aria-label='More options'
276 onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
288 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
289 const { name, description, uuid } = currentResource as ProjectResource;
290 const [showDescription, setShowDescription] = React.useState(false);
291 const [showProperties, setShowProperties] = React.useState(false);
293 const toggleDescription = () => {
294 setShowDescription(!showDescription);
297 const toggleProperties = () => {
298 setShowProperties(!showProperties);
303 className={classNames(classes.root, isSelected ? classes.selected : '')}
304 onClick={() => handleCardClick(uuid)}
305 data-cy='project-details-card'
308 className={classes.cardHeader}
310 <section className={classes.nameSection}>
311 <section className={classes.namePlate}>
315 style={{ marginRight: '1rem' }}
320 className={classes.faveIcon}
321 resourceUuid={currentResource.uuid}
324 className={classes.faveIcon}
325 resourceUuid={currentResource.uuid}
327 {!!frozenByFullName && (
329 className={classes.frozenIcon}
331 title={<span>Project was frozen by {frozenByFullName}</span>}
333 <FreezeIcon style={{ fontSize: 'inherit' }} />
340 <section className={classes.contextMenuSection}>
346 aria-label='More options'
347 onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
349 <MoreVerticalIcon data-cy='kebab-icon' />
355 <section onClick={(ev) => ev.stopPropagation()}>
358 onClick={toggleDescription}
359 className={classes.descriptionToggle}
361 <ExpandChevronRight expanded={showDescription} />
362 <section className={classes.showMore}>
366 collapsedHeight='1.25rem'
369 className={classes.description}
370 data-cy='project-description'
379 className={classes.noDescription}
380 data-cy='no-description'
382 no description available
385 {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
387 onClick={toggleProperties}
388 className={classes.descriptionToggle}
390 <ExpandChevronRight expanded={showProperties} />
391 <section className={classes.showMore}>
395 collapsedHeight='35px'
398 className={classes.description}
399 data-cy='project-description'
401 <CardContent className={classes.cardContent}>
402 <Typography component='div'>
403 {Object.keys(currentResource.properties).map((k) =>
404 Array.isArray(currentResource.properties[k])
405 ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
406 : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)