// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { Card, CardHeader, WithStyles, withStyles, Typography, CardContent } from '@material-ui/core';
+import { Card, CardHeader, WithStyles, withStyles, Typography, CardContent, Tooltip } from '@material-ui/core';
import { StyleRulesCallback } from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
import { RootState } from 'store/store';
import { connect } from 'react-redux';
import { getResource } from 'store/resources/resources';
import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar';
-import { DetailsAttribute } from 'components/details-attribute/details-attribute';
-import { RichTextEditorLink } from 'components/rich-text-editor-link/rich-text-editor-link';
import { getPropertyChip } from '../resource-properties-form/property-chip';
import { ProjectResource } from 'models/project';
-import { GroupClass } from 'models/group';
-import { ResourceWithName } from 'views-components/data-explorer/renderers';
-import { formatDate } from 'common/formatters';
-import { resourceLabel } from 'common/labels';
import { ResourceKind } from 'models/resource';
+import { UserResource } from 'models/user';
+import { UserResourceAccountStatus } from 'views-components/data-explorer/renderers';
+import { FavoriteStar, PublicFavoriteStar } from 'views-components/favorite-star/favorite-star';
+import { FreezeIcon } from 'components/icon/icon';
+import { Resource } from 'models/resource';
+import { MoreVerticalIcon } from 'components/icon/icon';
+import { IconButton } from '@material-ui/core';
+import { ContextMenuResource } from 'store/context-menu/context-menu-actions';
+import { resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
+import { openContextMenu } from 'store/context-menu/context-menu-actions';
+import { CollectionResource } from 'models/collection';
+import { RichTextEditorLink } from 'components/rich-text-editor-link/rich-text-editor-link';
-type CssRules = 'root' | 'cardheader' | 'fadeout' | 'cardcontent' | 'attributesection' | 'attribute' | 'chipsection' | 'tag';
+type CssRules =
+ | 'root'
+ | 'cardheader'
+ | 'fadeout'
+ | 'showmore'
+ | 'nameContainer'
+ | 'activeIndicator'
+ | 'cardcontent'
+ | 'namePlate'
+ | 'faveIcon'
+ | 'frozenIcon'
+ | 'attributesection'
+ | 'attribute'
+ | 'chipsection'
+ | 'tag';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
width: '100%',
marginBottom: '1rem',
+ flex: '0 0 auto',
},
fadeout: {
- maxWidth: '30rem',
+ maxWidth: '25rem',
minWdidth: '18rem',
- height: '2.7rem',
+ height: '1.5rem',
overflow: 'hidden',
- WebkitMaskImage: '-webkit-gradient(linear, left top, left bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0)))',
+ WebkitMaskImage: '-webkit-gradient(linear, left bottom, right bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0)))',
+ },
+ showmore: {
+ color: theme.palette.primary.main,
+ cursor: 'pointer',
+ maxWidth: '10rem',
+ },
+ nameContainer: {
+ display: 'flex',
+ },
+ activeIndicator: {
+ margin: '0.3rem auto auto 1rem',
},
cardheader: {
paddingTop: '0.4rem',
cardcontent: {
display: 'flex',
flexDirection: 'column',
- marginTop: '-1rem'
+ marginTop: '-1rem',
+ },
+ namePlate: {
+ display: 'flex',
+ flexDirection: 'row',
+ },
+ faveIcon: {
+ fontSize: '0.8rem',
+ margin: 'auto 0 0.5rem 0.3rem',
+ color: theme.palette.text.primary,
+ },
+ frozenIcon: {
+ fontSize: '0.5rem',
+ marginLeft: '0.3rem',
+ marginTop: '0.57rem',
+ height: '1rem',
+ color: theme.palette.text.primary,
},
attributesection: {
display: 'flex',
attribute: {
marginBottom: '0.5rem',
marginRight: '1rem',
- border: '1px solid lightgrey',
padding: '0.5rem',
- borderRadius: '5px'
+ borderRadius: '5px',
},
chipsection: {
display: 'flex',
},
tag: {
marginRight: '1rem',
- marginTop: '0.5rem'
+ marginTop: '0.5rem',
},
});
const currentRoute = state.router.location?.pathname.split('/') || [];
const currentItemUuid = currentRoute[currentRoute.length - 1];
const currentResource = getResource(currentItemUuid)(state.resources);
+ const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(state.resources);
+ const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
+
return {
+ isAdmin: state.auth.user?.isAdmin,
currentResource,
+ frozenByFullName,
};
};
-type DetailsCardProps = {
+const mapDispatchToProps = (dispatch: any) => ({
+ handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: any, isAdmin: boolean) => {
+ // When viewing the contents of a filter group, all contents should be treated as read only.
+ let readOnly = false;
+ if (resource.groupClass === 'filter') {
+ readOnly = true;
+ }
+
+ const menuKind = dispatch(resourceUuidToContextMenuKind(resource.uuid, readOnly));
+ if (menuKind && resource) {
+ dispatch(
+ openContextMenu(event, {
+ name: resource.name,
+ uuid: resource.uuid,
+ ownerUuid: resource.ownerUuid,
+ isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
+ kind: resource.kind,
+ menuKind,
+ isAdmin,
+ isFrozen: !!resource.frozenByUuid,
+ description: resource.description,
+ storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
+ properties: 'properties' in resource ? resource.properties : {},
+ })
+ );
+ }
+
+ },
+});
+
+type DetailsCardProps = WithStyles<CssRules> & {
+ currentResource: ProjectResource | UserResource;
+ frozenByFullName?: string;
+ isAdmin: boolean;
+ handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
+};
+
+type UserCardProps = WithStyles<CssRules> & {
+ currentResource: UserResource;
+};
+
+type ProjectCardProps = WithStyles<CssRules> & {
currentResource: ProjectResource;
+ frozenByFullName: string | undefined;
+ isAdmin: boolean;
+ handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
};
-export const ProjectDetailsCard = connect(mapStateToProps)(
- withStyles(styles)((props: DetailsCardProps & WithStyles<CssRules>) => {
- const { classes, currentResource } = props;
- const { name, description, uuid } = currentResource;
- return (
- <Card className={classes.root}>
- <CardHeader
+export const ProjectDetailsCard = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(
+ withStyles(styles)((props: DetailsCardProps) => {
+ const { classes, currentResource, frozenByFullName, handleContextMenu, isAdmin } = props;
+ switch (currentResource.kind as string) {
+ case ResourceKind.USER:
+ return (
+ <UserCard
+ classes={classes}
+ currentResource={currentResource as UserResource}
+ />
+ );
+ case ResourceKind.PROJECT:
+ return (
+ <ProjectCard
+ classes={classes}
+ currentResource={currentResource as ProjectResource}
+ frozenByFullName={frozenByFullName}
+ isAdmin={isAdmin}
+ handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
+ />
+ );
+ default:
+ return null;
+ }
+ })
+);
+
+const UserCard: React.FC<UserCardProps> = ({ classes, currentResource }) => {
+ const { fullName, uuid } = currentResource as UserResource & { fullName: string };
+
+ return (
+ <Card className={classes.root}>
+ <CardHeader
className={classes.cardheader}
- title={
+ title={
+ <section className={classes.nameContainer}>
<Typography
noWrap
variant='h6'
>
- {name}
- </Typography>
- }
- subheader={
- description ? (
- <section>
- <Typography className={classes.fadeout}>{description.replace(/<[^>]*>/g, '')}</Typography>
- <RichTextEditorLink
- title={`Description of ${name}`}
- content={description}
- label='Show full description'
- />
- </section>
- ) : (
- '---'
- )
- }
- action={<MultiselectToolbar inputSelectedUuid={uuid} />}
- />
- <CardContent className={classes.cardcontent}>
- <section className={classes.attributesection}>
- <Typography
- component='div'
- className={classes.attribute}
- >
- <DetailsAttribute
- label='Type'
- value={currentResource.groupClass === GroupClass.FILTER ? 'Filter group' : resourceLabel(ResourceKind.PROJECT)}
- />
- </Typography>
- <Typography
- component='div'
- className={classes.attribute}
- >
- <DetailsAttribute
- label='Owner'
- linkToUuid={currentResource.ownerUuid}
- uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />}
- />
+ {fullName}
</Typography>
+ {!currentResource.isActive && (
+ <Typography className={classes.activeIndicator}>
+ <UserResourceAccountStatus uuid={uuid} />
+ </Typography>
+ )}
+ </section>
+ }
+ action={<MultiselectToolbar inputSelectedUuid={uuid} />}
+ />
+ </Card>
+ );
+};
+
+const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleContextMenu, isAdmin }) => {
+ const { name, uuid, description } = currentResource as ProjectResource;
+
+ return (
+ <Card className={classes.root}>
+ <CardHeader
+ className={classes.cardheader}
+ title={
+ <>
+ <section className={classes.namePlate}>
<Typography
- component='div'
- className={classes.attribute}
+ noWrap
+ variant='h6'
+ style={{ marginRight: '1rem' }}
>
- <DetailsAttribute
- label='Last modified'
- value={formatDate(currentResource.modifiedAt)}
- />
+ {name}
</Typography>
- <Typography
- component='div'
- className={classes.attribute}
+ <FavoriteStar
+ className={classes.faveIcon}
+ resourceUuid={currentResource.uuid}
+ />
+ <PublicFavoriteStar
+ className={classes.faveIcon}
+ resourceUuid={currentResource.uuid}
+ />
+ {!!frozenByFullName && <Tooltip
+ className={classes.frozenIcon}
+ title={<span>Project was frozen by {frozenByFullName}</span>}
>
- <DetailsAttribute
- label='Created at'
- value={formatDate(currentResource.createdAt)}
- />
- </Typography>
- <Typography
- component='div'
- className={classes.attribute}
- >
- <DetailsAttribute
- label='UUID'
- linkToUuid={currentResource.uuid}
- value={currentResource.uuid}
- />
- </Typography>
+ <FreezeIcon style={{ fontSize: 'inherit' }} />
+ </Tooltip>}
</section>
<section className={classes.chipsection}>
- <Typography
- component='div'
- // className={classes.attribute}
- >
- {Object.keys(currentResource.properties).map((k) =>
+ <Typography component='div'>
+ {typeof currentResource.properties === 'object' &&
+ Object.keys(currentResource.properties).map((k) =>
Array.isArray(currentResource.properties[k])
? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
: getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
)}
- </Typography>
- </section>
- </CardContent>
- </Card>
- );
- })
-);
+ </Typography>
+ </section>
+ </>
+ }
+
+
+ action={<Tooltip
+ title='More options'
+ disableFocusListener
+ >
+ <IconButton
+ aria-label='More options'
+ onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
+ >
+ <MoreVerticalIcon />
+ </IconButton>
+ </Tooltip>}
+ />
+ <CardContent className={classes.cardcontent}>
+ {description && (
+ <section>
+ {/* <Typography className={classes.fadeout}>{description.replace(/<[^>]*>/g, '').slice(0, 45)}...</Typography> */}
+ <div className={classes.showmore}>
+ <RichTextEditorLink
+ title={`Description of ${name}`}
+ content={description}
+ label='Show full description'
+ />
+ </div>
+ </section>
+ )}
+ </CardContent>
+ </Card>
+ );
+};