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 { ContextMenuResource } from 'store/context-menu/context-menu-actions';
20 import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
21 import { CollectionResource } from 'models/collection';
22 import { ContextMenuKind } from 'views-components/context-menu/context-menu';
23 import { Dispatch } from 'redux';
24 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
25 import { ExpandChevronRight } from 'components/expand-chevron-right/expand-chevron-right';
26 import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar';
27 import { setSelectedResourceUuid } from 'store/selected-resource/selected-resource-actions';
28 import { deselectAllOthers } from 'store/multiselect/multiselect-actions';
32 | 'cardHeaderContainer'
43 | 'accountStatusSection'
47 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
59 color: theme.palette.grey['600'],
67 cardHeaderContainer: {
71 justifyContent: 'space-between',
75 padding: '0.2rem 0.4rem 0.2rem 1rem',
81 paddingBottom: '0.5rem',
85 flexDirection: 'column',
87 paddingBottom: '-1rem',
88 paddingLeft: '0.5rem',
103 margin: 'auto 0 0.5rem 0.3rem',
104 color: theme.palette.text.primary,
108 marginLeft: '0.3rem',
111 color: theme.palette.text.primary,
113 accountStatusSection: {
115 flexDirection: 'row',
116 alignItems: 'center',
120 marginRight: '0.75rem',
121 marginBottom: '0.5rem',
129 const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => {
130 const currentResource = getResource(properties.currentRouteUuid)(resources);
131 const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
132 const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
133 const isSelected = selectedResourceUuid === properties.currentRouteUuid;
136 isAdmin: auth.user?.isAdmin,
143 const mapDispatchToProps = (dispatch: Dispatch) => ({
144 handleCardClick: (uuid: string) => {
145 dispatch<any>(loadDetailsPanel(uuid));
146 dispatch<any>(setSelectedResourceUuid(uuid));
147 dispatch<any>(deselectAllOthers(uuid));
149 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: any, isAdmin: boolean) => {
150 event.stopPropagation();
151 // When viewing the contents of a filter group, all contents should be treated as read only.
152 let readOnly = false;
153 if (resource.groupClass === 'filter') {
156 let menuKind = dispatch<any>(resourceUuidToContextMenuKind(resource.uuid, readOnly));
157 if (menuKind === ContextMenuKind.ROOT_PROJECT) {
158 menuKind = ContextMenuKind.USER_DETAILS;
160 if (menuKind && resource) {
162 openContextMenu(event, {
165 ownerUuid: resource.ownerUuid,
166 isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
170 isFrozen: !!resource.frozenByUuid,
171 description: resource.description,
172 storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
173 properties: 'properties' in resource ? resource.properties : {},
180 type DetailsCardProps = WithStyles<CssRules> & {
181 currentResource: ProjectResource | UserResource;
182 frozenByFullName?: string;
185 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
186 handleCardClick: (resource: any) => void;
189 type UserCardProps = WithStyles<CssRules> & {
190 currentResource: UserResource;
193 handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
194 handleCardClick: (resource: any) => void;
197 type ProjectCardProps = WithStyles<CssRules> & {
198 currentResource: ProjectResource;
199 frozenByFullName: string | undefined;
202 handleCardClick: (resource: any) => void;
205 export const ProjectDetailsCard = connect(
209 withStyles(styles)((props: DetailsCardProps) => {
210 const { classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected } = props;
211 if (!currentResource) {
214 switch (currentResource.kind as string) {
215 case ResourceKind.USER:
219 currentResource={currentResource as UserResource}
221 isSelected={isSelected}
222 handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
223 handleCardClick={handleCardClick}
226 case ResourceKind.PROJECT:
230 currentResource={currentResource as ProjectResource}
231 frozenByFullName={frozenByFullName}
233 isSelected={isSelected}
234 handleCardClick={handleCardClick}
243 const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleCardClick, isSelected }) => {
244 const { fullName, uuid } = currentResource as UserResource & { fullName: string };
248 className={classes.root}
249 onClick={() => handleCardClick(uuid)}
250 data-cy='user-details-card'
255 className={classes.cardHeaderContainer}
258 className={classes.cardHeader}
260 <section className={classes.userNameContainer}>
267 <section className={classes.accountStatusSection}>
268 {!currentResource.isActive && (
270 <UserResourceAccountStatus uuid={uuid} />
277 {isSelected && <MultiselectToolbar />}
283 const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleCardClick, isSelected }) => {
284 const { name, description, uuid } = currentResource as ProjectResource;
285 const [showDescription, setShowDescription] = React.useState(false);
286 const [showProperties, setShowProperties] = React.useState(false);
288 const toggleDescription = () => {
289 console.log(showDescription, showProperties);
290 setShowDescription(!showDescription);
293 const toggleProperties = () => {
294 setShowProperties(!showProperties);
299 className={classes.root}
300 onClick={() => handleCardClick(uuid)}
301 data-cy='project-details-card'
306 className={classes.cardHeaderContainer}
309 className={classes.cardHeader}
311 <section className={classes.nameSection}>
312 <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 {isSelected && <MultiselectToolbar />}
342 <section onClick={(ev) => ev.stopPropagation()}>
345 onClick={toggleDescription}
346 className={classes.descriptionToggle}
348 <ExpandChevronRight expanded={showDescription} />
349 <section className={classes.showMore}>
353 collapsedHeight='1.25rem'
356 className={classes.description}
357 data-cy='project-description'
366 className={classes.noDescription}
367 data-cy='no-description'
369 no description available
372 {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
374 onClick={toggleProperties}
375 className={classes.descriptionToggle}
377 <ExpandChevronRight expanded={showProperties} />
378 <section className={classes.showMore}>
382 collapsedHeight='35px'
385 className={classes.description}
386 data-cy='project-description'
388 <CardContent className={classes.cardContent}>
389 <Typography component='div'>
390 {Object.keys(currentResource.properties).map((k) =>
391 Array.isArray(currentResource.properties[k])
392 ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
393 : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)