21224: set project card to only display 3 buttons Arvados-DCO-1.1-Signed-off-by:...
[arvados.git] / services / workbench2 / src / views-components / project-details-card / project-details-card.tsx
index b75cb9e8871b53351e21bf8645263d4507c1d074..49884e45dee7eec241bd78ba170c6038d2351acc 100644 (file)
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { Card, CardHeader, WithStyles, withStyles, Typography, CardContent } from '@material-ui/core';
-import { StyleRulesCallback } from '@material-ui/core';
+import { StyleRulesCallback, Card, CardHeader, WithStyles, withStyles, Typography, CardContent, Tooltip, Collapse, Grid } 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 { Dispatch } from 'redux';
+import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
+import { ExpandChevronRight } from 'components/expand-chevron-right/expand-chevron-right';
+import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar';
+import { setSelectedResourceUuid } from 'store/selected-resource/selected-resource-actions';
+import { deselectAllOthers } from 'store/multiselect/multiselect-actions';
 
-
-
-type CssRules = 'root' | 'cardheader' | 'fadeout' | 'nameContainer' | 'activeIndicator' | 'cardcontent' | 'attributesection' | 'attribute' | 'chipsection' | 'tag';
+type CssRules =
+    | 'root'
+    | 'cardHeaderContainer'
+    | 'cardHeader'
+    | 'projectToolbar'
+    | 'descriptionToggle'
+    | 'showMore'
+    | 'noDescription'
+    | 'userNameContainer'
+    | 'cardContent'
+    | 'nameSection'
+    | 'namePlate'
+    | 'faveIcon'
+    | 'frozenIcon'
+    | 'accountStatusSection'
+    | 'chipSection'
+    | 'tag'
+    | 'description';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         width: '100%',
         marginBottom: '1rem',
+        flex: '0 0 auto',
+        padding: 0,
+        minHeight: '3rem',
+    },
+    showMore: {
+        cursor: 'pointer',
     },
-    fadeout: {
-        maxWidth: '25rem',
-        minWdidth: '18rem',
-        height: '1.5rem',
-        overflow: 'hidden',
-        WebkitMaskImage: '-webkit-gradient(linear, left bottom, right bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0)))',
+    noDescription: {
+        color: theme.palette.grey['600'],
+        fontStyle: 'italic',
+        padding: '0  0 0.5rem 1rem',
+        marginTop: '-0.5rem',
     },
-    nameContainer: {
+    userNameContainer: {
         display: 'flex',
+        alignItems: 'center',
+        minHeight: '2.7rem',
     },
-    activeIndicator: {
-        margin: '0.3rem auto auto 1rem',
+    cardHeaderContainer: {
+        width: '100%',
+        display: 'flex',
+        flexDirection: 'row',
+        justifyContent: 'space-between',
     },
-    cardheader: {
-        paddingTop: '0.4rem',
+    cardHeader: {
+        minWidth: '30rem',
+        padding: '0.2rem 0.4rem 0.2rem 1rem',
     },
-    cardcontent: {
+    projectToolbar: {
+        //shows only the first 3 buttons
+        width: '12rem !important',
+    },
+    descriptionToggle: {
+        display: 'flex',
+        flexDirection: 'row',
+        cursor: 'pointer',
+        paddingBottom: '0.5rem',
+    },
+    cardContent: {
         display: 'flex',
         flexDirection: 'column',
-        marginTop: '-1rem',
+        paddingTop: 0,
+        paddingLeft: '0.1rem',
     },
-    attributesection: {
+    nameSection: {
         display: 'flex',
-        flexWrap: 'wrap',
+        flexDirection: 'row',
+        alignItems: 'center',
     },
-    attribute: {
-        marginBottom: '0.5rem',
-        marginRight: '1rem',
-        border: '1px solid lightgrey',
-        padding: '0.5rem',
-        borderRadius: '5px',
+    namePlate: {
+        display: 'flex',
+        flexDirection: 'row',
+        alignItems: 'center',
+        margin: 0,
+        minHeight: '2.7rem',
+    },
+    faveIcon: {
+        fontSize: '0.8rem',
+        margin: 'auto 0 1rem 0.3rem',
+        color: theme.palette.text.primary,
     },
-    chipsection: {
+    frozenIcon: {
+        fontSize: '0.5rem',
+        marginLeft: '0.3rem',
+        height: '1rem',
+        color: theme.palette.text.primary,
+    },
+    accountStatusSection: {
         display: 'flex',
-        flexWrap: 'wrap',
+        flexDirection: 'row',
+        alignItems: 'center',
+        paddingLeft: '1rem',
+    },
+    chipSection: {
+        marginBottom: '-2rem',
     },
     tag: {
-        marginRight: '1rem',
-        marginTop: '0.5rem',
+        marginRight: '0.75rem',
+        marginBottom: '0.5rem',
+    },
+    description: {
+        maxWidth: '95%',
+        marginTop: 0,
     },
 });
 
-const mapStateToProps = (state: RootState) => {
-    const currentRoute = state.router.location?.pathname.split('/') || [];
-    const currentItemUuid = currentRoute[currentRoute.length - 1];
-    const currentResource = getResource(currentItemUuid)(state.resources);
+const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => {
+    const currentResource = getResource(properties.currentRouteUuid)(resources);
+    const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
+    const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
+    const isSelected = selectedResourceUuid === properties.currentRouteUuid;
+
     return {
+        isAdmin: auth.user?.isAdmin,
         currentResource,
+        frozenByFullName,
+        isSelected,
     };
 };
 
-type DetailsCardProps = {
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    handleCardClick: (uuid: string) => {
+        dispatch<any>(loadDetailsPanel(uuid));
+        dispatch<any>(setSelectedResourceUuid(uuid));
+        dispatch<any>(deselectAllOthers(uuid));
+    },
+    
+});
+
+type DetailsCardProps = WithStyles<CssRules> & {
     currentResource: ProjectResource | UserResource;
+    frozenByFullName?: string;
+    isAdmin: boolean;
+    isSelected: boolean;
+    handleCardClick: (resource: any) => void;
+};
+
+type UserCardProps = WithStyles<CssRules> & {
+    currentResource: UserResource;
+    isAdmin: boolean;
+    isSelected: boolean;
+    handleCardClick: (resource: any) => void;
+};
+
+type ProjectCardProps = WithStyles<CssRules> & {
+    currentResource: ProjectResource;
+    frozenByFullName: string | undefined;
+    isAdmin: boolean;
+    isSelected: boolean;
+    handleCardClick: (resource: any) => void;
 };
 
-export const ProjectDetailsCard = connect(mapStateToProps)(
-    withStyles(styles)((props: DetailsCardProps & WithStyles<CssRules>) => {
-        const { currentResource } = props;
-        return (currentResource.kind as string) === ResourceKind.USER ? <UserCard props={props} /> : <ProjectCard props={props} />;
+export const ProjectDetailsCard = connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(
+    withStyles(styles)((props: DetailsCardProps) => {
+        const { classes, currentResource, frozenByFullName, handleCardClick, isAdmin, isSelected } = props;
+        if (!currentResource) {
+            return null;
+        }
+        switch (currentResource.kind as string) {
+            case ResourceKind.USER:
+                return (
+                    <UserCard
+                        classes={classes}
+                        currentResource={currentResource as UserResource}
+                        isAdmin={isAdmin}
+                        isSelected={isSelected}
+                        handleCardClick={handleCardClick}
+                    />
+                );
+            case ResourceKind.PROJECT:
+                return (
+                    <ProjectCard
+                        classes={classes}
+                        currentResource={currentResource as ProjectResource}
+                        frozenByFullName={frozenByFullName}
+                        isAdmin={isAdmin}
+                        isSelected={isSelected}
+                        handleCardClick={handleCardClick}
+                    />
+                );
+            default:
+                return null;
+        }
     })
 );
 
-const UserCard = ({ props }) => {
-    const { classes, currentResource } = props;
-    const { fullName, uuid, username, email, isAdmin } = currentResource as UserResource & { fullName: string };
+const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleCardClick, isSelected }) => {
+    const { fullName, uuid } = currentResource as UserResource & { fullName: string };
 
     return (
-        <Card className={classes.root}>
-            <CardHeader
-                className={classes.cardheader}
-                title={
-                    <section className={classes.nameContainer}>
-                        <Typography
-                            noWrap
-                            variant='h6'
+        <Card
+            className={classes.root}
+            onClick={() => handleCardClick(uuid)}
+            data-cy='user-details-card'
+        >
+            <Grid
+                container
+                wrap='nowrap'
+                className={classes.cardHeaderContainer}
+            >
+                <CardHeader
+                    className={classes.cardHeader}
+                    title={
+                        <section className={classes.userNameContainer}>
+                            <Typography
+                                noWrap
+                                variant='h6'
                             >
-                            {fullName}
-                        </Typography>
-                        <Typography
-                            className={classes.activeIndicator}
-                        >
-                            <UserResourceAccountStatus uuid={uuid} />
-                        </Typography>
-                    </section>
-                }
-                action={
-                <MultiselectToolbar inputSelectedUuid={uuid} />
-                }
-            />
-            <CardContent className={classes.cardcontent}>
-                <section className={classes.attributesection}>
-                    <Typography
-                        component='div'
-                        className={classes.attribute}
-                    >
-                        <DetailsAttribute
-                            label='Username'
-                            value={username}
-                        />
-                    </Typography>
-                    <Typography
-                        component='div'
-                        className={classes.attribute}
-                    >
-                        <DetailsAttribute
-                            label='Email'
-                            value={email}
-                        />
-                    </Typography>
-                    <Typography
-                        component='div'
-                        className={classes.attribute}
-                    >
-                        <DetailsAttribute
-                            label='Admin'
-                            value={isAdmin ? 'Yes' : 'No'}
-                        />
-                    </Typography>
-                    <Typography
-                        component='div'
-                        className={classes.attribute}
-                    >
-                        <DetailsAttribute
-                            label='UUID'
-                            linkToUuid={currentResource.uuid}
-                            value={currentResource.uuid}
-                        />
-                    </Typography>
-                </section>
-            </CardContent>
+                                {fullName}
+                            </Typography>
+                            <section className={classes.accountStatusSection}>
+                                {!currentResource.isActive && (
+                                    <Typography>
+                                        <UserResourceAccountStatus uuid={uuid} />
+                                    </Typography>
+                            )}
+                            </section>
+                        </section>
+                    }
+                />
+                {isSelected && <MultiselectToolbar />}
+            </Grid>
         </Card>
     );
 };
 
-const ProjectCard = ({ props }) => {
-    const { classes, currentResource } = props;
-    const { name, uuid, description } = currentResource as ProjectResource;
+const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleCardClick, isSelected }) => {
+    const { name, description, uuid } = currentResource as ProjectResource;
+    const [showDescription, setShowDescription] = React.useState(false);
+    const [showProperties, setShowProperties] = React.useState(false);
+
+    const toggleDescription = () => {
+        setShowDescription(!showDescription);
+    };
+
+    const toggleProperties = () => {
+        setShowProperties(!showProperties);
+    };
+
     return (
-        <Card className={classes.root}>
-            <CardHeader
-                className={classes.cardheader}
-                title={
-                    <Typography
-                        noWrap
-                        variant='h6'
-                    >
-                        {name}
-                    </Typography>
-                }
-                subheader={
-                    description ? (
-                        <section>
-                            <Typography className={classes.fadeout}>{description.replace(/<[^>]*>/g, '').slice(0, 45)}...</Typography>
-                            <RichTextEditorLink
-                                title={`Description of ${name}`}
-                                content={description}
-                                label='Show full description'
-                            />
+        <Card
+            className={classes.root}
+            onClick={() => handleCardClick(uuid)}
+            data-cy='project-details-card'
+        >
+            <Grid
+                container
+                wrap='nowrap'
+                className={classes.cardHeaderContainer}
+            >
+                <CardHeader
+                    className={classes.cardHeader}
+                    title={
+                        <section className={classes.nameSection}>
+                            <section className={classes.namePlate}>
+                                <Typography
+                                    variant='h6'
+                                    style={{ marginRight: '1rem' }}
+                                >
+                                    {name}
+                                </Typography>
+                                <FavoriteStar
+                                    className={classes.faveIcon}
+                                    resourceUuid={currentResource.uuid}
+                                />
+                                <PublicFavoriteStar
+                                    className={classes.faveIcon}
+                                    resourceUuid={currentResource.uuid}
+                                />
+                                {!!frozenByFullName && (
+                                    <Tooltip
+                                        className={classes.frozenIcon}
+                                        disableFocusListener
+                                        title={<span>Project was frozen by {frozenByFullName}</span>}
+                                    >
+                                        <FreezeIcon style={{ fontSize: 'inherit' }} />
+                                    </Tooltip>
+                                )}
+                            </section>
                         </section>
-                    ) : (
-                        'no description available'
-                    )
-                }
-                action={<MultiselectToolbar inputSelectedUuid={uuid} />}
-                
-            />
-            <CardContent className={classes.cardcontent}>
-                <section className={classes.attributesection}>
-                    <Typography
-                        component='div'
-                        className={classes.attribute}
+                    }
+                />
+                {isSelected && <MultiselectToolbar injectedStyles={classes.projectToolbar} />}
+            </Grid>
+            <section onClick={(ev) => ev.stopPropagation()}>
+                {description ? (
+                    <section
+                        onClick={toggleDescription}
+                        className={classes.descriptionToggle}
                     >
-                        <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} />}
-                        />
-                    </Typography>
-                    <Typography
-                        component='div'
-                        className={classes.attribute}
-                    >
-                        <DetailsAttribute
-                            label='Last modified'
-                            value={formatDate(currentResource.modifiedAt)}
-                        />
-                    </Typography>
+                        <ExpandChevronRight expanded={showDescription} />
+                        <section className={classes.showMore}>
+                            <Collapse
+                                in={showDescription}
+                                timeout='auto'
+                                collapsedHeight='1.25rem'
+                            >
+                                <Typography
+                                    className={classes.description}
+                                    data-cy='project-description'
+                                    //dangerouslySetInnerHTML is ok here only if description is sanitized,
+                                    //which it is before it is loaded into the redux store
+                                    dangerouslySetInnerHTML={{ __html: description }}
+                                />
+                            </Collapse>
+                        </section>
+                    </section>
+                ) : (
                     <Typography
-                        component='div'
-                        className={classes.attribute}
+                        className={classes.noDescription}
+                        data-cy='no-description'
                     >
-                        <DetailsAttribute
-                            label='Created at'
-                            value={formatDate(currentResource.createdAt)}
-                        />
+                        no description available
                     </Typography>
-                    <Typography
-                        component='div'
-                        className={classes.attribute}
+                )}
+                {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
+                    <section
+                        onClick={toggleProperties}
+                        className={classes.descriptionToggle}
                     >
-                        <DetailsAttribute
-                            label='UUID'
-                            linkToUuid={currentResource.uuid}
-                            value={currentResource.uuid}
-                        />
-                    </Typography>
-                </section>
-                <section className={classes.chipsection}>
-                    <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>
+                        <ExpandChevronRight expanded={showProperties} />
+                        <section className={classes.showMore}>
+                            <Collapse
+                                in={showProperties}
+                                timeout='auto'
+                                collapsedHeight='35px'
+                            >
+                                <div
+                                    className={classes.description}
+                                    data-cy='project-description'
+                                >
+                                    <CardContent className={classes.cardContent}>
+                                        <Typography component='div' className={classes.chipSection}>
+                                            {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>
+                                    </CardContent>
+                                </div>
+                            </Collapse>
+                        </section>
+                    </section>
+                ) : null}
+            </section>
         </Card>
     );
 };