21224: fixed project panel to viewport height Arvados-DCO-1.1-Signed-off-by: Lisa...
[arvados.git] / services / workbench2 / src / views-components / project-details-card / project-details-card.tsx
index d9db66d6341c06b757df4c74d6d34067cdf333a3..06a76c713e616240e9ab9d018fdd24e808f66dee 100644 (file)
@@ -3,36 +3,68 @@
 // 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',
@@ -40,7 +72,23 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     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',
@@ -49,9 +97,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     attribute: {
         marginBottom: '0.5rem',
         marginRight: '1rem',
-        border: '1px solid lightgrey',
         padding: '0.5rem',
-        borderRadius: '5px'
+        borderRadius: '5px',
     },
     chipsection: {
         display: 'flex',
@@ -59,7 +106,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     tag: {
         marginRight: '1rem',
-        marginTop: '0.5rem'
+        marginTop: '0.5rem',
     },
 });
 
@@ -67,111 +114,194 @@ const mapStateToProps = (state: RootState) => {
     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>
+    );
+};