X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/5b04f19b3c8d308abe01644d83726b63a483364a..92a147d8e4fd5b02264c06ec432255777cb942c7:/services/workbench2/src/views-components/project-details-card/project-details-card.tsx?ds=sidebyside diff --git a/services/workbench2/src/views-components/project-details-card/project-details-card.tsx b/services/workbench2/src/views-components/project-details-card/project-details-card.tsx index b75cb9e887..0988d7c168 100644 --- a/services/workbench2/src/views-components/project-details-card/project-details-card.tsx +++ b/services/workbench2/src/views-components/project-details-card/project-details-card.tsx @@ -3,73 +3,123 @@ // 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 } 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 { MoreVerticalIcon, FreezeIcon } from 'components/icon/icon'; +import { Resource } from 'models/resource'; +import { IconButton } from '@material-ui/core'; +import { ContextMenuResource } from 'store/context-menu/context-menu-actions'; +import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions'; +import { CollectionResource } from 'models/collection'; +import { ContextMenuKind } from 'views-components/context-menu/context-menu'; +import { Dispatch } from 'redux'; +import classNames from 'classnames'; +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'; - - -type CssRules = 'root' | 'cardheader' | 'fadeout' | 'nameContainer' | 'activeIndicator' | 'cardcontent' | 'attributesection' | 'attribute' | 'chipsection' | 'tag'; +type CssRules = + | 'root' + | 'selected' + | 'cardHeader' + | 'descriptionToggle' + | 'showMore' + | 'noDescription' + | 'userNameContainer' + | 'cardContent' + | 'nameSection' + | 'namePlate' + | 'faveIcon' + | 'frozenIcon' + | 'contextMenuSection' + | 'tag' + | 'description'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ root: { width: '100%', marginBottom: '1rem', + flex: '0 0 auto', + padding: 0, + border: '2px solid transparent', + }, + selected: { + border: '2px solid #ccc', }, - 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)))', + showMore: { + cursor: 'pointer', + background: 'linear-gradient(to right, black, transparent)', + backgroundClip: 'text', + color: 'transparent', }, - nameContainer: { + noDescription: { + color: theme.palette.grey['600'], + fontStyle: 'italic', + }, + userNameContainer: { display: 'flex', }, - activeIndicator: { - margin: '0.3rem auto auto 1rem', + cardHeader: { + padding: '0.2rem 0.4rem 0.1rem 1rem', }, - cardheader: { - paddingTop: '0.4rem', + descriptionToggle: { + display: 'flex', + flexDirection: 'row', + cursor: 'pointer', + paddingBottom: '0.5rem', }, - cardcontent: { + cardContent: { display: 'flex', flexDirection: 'column', - marginTop: '-1rem', + marginTop: '-1.75rem', }, - attributesection: { + nameSection: { display: 'flex', - flexWrap: 'wrap', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', }, - attribute: { - marginBottom: '0.5rem', - marginRight: '1rem', - border: '1px solid lightgrey', - padding: '0.5rem', - borderRadius: '5px', + namePlate: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + margin: 0, + paddingBottom: '0.5rem', + }, + faveIcon: { + fontSize: '0.8rem', + margin: 'auto 0 0.5rem 0.3rem', + color: theme.palette.text.primary, }, - chipsection: { + frozenIcon: { + fontSize: '0.5rem', + marginLeft: '0.3rem', + marginTop: '0.1rem', + height: '1rem', + color: theme.palette.text.primary, + }, + contextMenuSection: { display: 'flex', - flexWrap: 'wrap', + flexDirection: 'row', + alignItems: 'center', + paddingTop: '0.25rem', }, tag: { marginRight: '1rem', - marginTop: '0.5rem', + marginTop: '1rem', + }, + description: { + maxWidth: '90%', + marginTop: 0, }, }); @@ -77,187 +127,294 @@ 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; + const isSelected = currentItemUuid === state.detailsPanel.resourceUuid && state.detailsPanel.isOpened === true; + return { + isAdmin: state.auth.user?.isAdmin, currentResource, + frozenByFullName, + isSelected, }; }; -type DetailsCardProps = { +const mapDispatchToProps = (dispatch: Dispatch) => ({ + handleCardClick: (uuid: string) => { + dispatch(loadDetailsPanel(uuid)); + }, + handleContextMenu: (event: React.MouseEvent, resource: any, isAdmin: boolean) => { + event.stopPropagation(); + // 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; + } + let menuKind = dispatch(resourceUuidToContextMenuKind(resource.uuid, readOnly)); + if (menuKind === ContextMenuKind.ROOT_PROJECT) { + menuKind = ContextMenuKind.USER_DETAILS; + } + 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 & { currentResource: ProjectResource | UserResource; + frozenByFullName?: string; + isAdmin: boolean; + isSelected: boolean; + handleContextMenu: (event: React.MouseEvent, resource: ContextMenuResource, isAdmin: boolean) => void; + handleCardClick: (resource: any) => void; +}; + +type UserCardProps = WithStyles & { + currentResource: UserResource; + isAdmin: boolean; + isSelected: boolean; + handleContextMenu: (event: React.MouseEvent, resource: ContextMenuResource, isAdmin: boolean) => void; + handleCardClick: (resource: any) => void; }; -export const ProjectDetailsCard = connect(mapStateToProps)( - withStyles(styles)((props: DetailsCardProps & WithStyles) => { - const { currentResource } = props; - return (currentResource.kind as string) === ResourceKind.USER ? : ; +type ProjectCardProps = WithStyles & { + currentResource: ProjectResource; + frozenByFullName: string | undefined; + isAdmin: boolean; + isSelected: boolean; + handleContextMenu: (event: React.MouseEvent, resource: ContextMenuResource, isAdmin: boolean) => void; + handleCardClick: (resource: any) => void; +}; + +export const ProjectDetailsCard = connect( + mapStateToProps, + mapDispatchToProps +)( + withStyles(styles)((props: DetailsCardProps) => { + const { classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected } = props; + if (!currentResource) { + return null; + } + switch (currentResource.kind as string) { + case ResourceKind.USER: + return ( + handleContextMenu(ev, currentResource as any, isAdmin)} + handleCardClick={handleCardClick} + /> + ); + case ResourceKind.PROJECT: + return ( + handleContextMenu(ev, currentResource as any, isAdmin)} + 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 = ({ classes, currentResource, handleContextMenu, handleCardClick, isAdmin, isSelected }) => { + const { fullName, uuid } = currentResource as UserResource & { fullName: string }; return ( - + handleCardClick(uuid)} + data-cy='user-details-card' + > +
- {fullName} - - - + {fullName}
} action={ - +
+ {!currentResource.isActive && ( + + + + )} + + handleContextMenu(ev, currentResource as any, isAdmin)} + > + + + +
} /> - -
- - - - - - - - - - - - -
-
); }; -const ProjectCard = ({ props }) => { - const { classes, currentResource } = props; - const { name, uuid, description } = currentResource as ProjectResource; +const ProjectCard: React.FC = ({ classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, 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 ( - + handleCardClick(uuid)} + data-cy='project-details-card' + > - {name} - - } - subheader={ - description ? ( -
- {description.replace(/<[^>]*>/g, '').slice(0, 45)}... - +
+ + {name} + + + + {!!frozenByFullName && ( + Project was frozen by {frozenByFullName}} + > + + + )}
- ) : ( - 'no description available' - ) +
+ } + action={ + + //
+ // + // handleContextMenu(ev, currentResource as any, isAdmin)} + // > + // + // + // + //
} - action={} - /> - -
- ev.stopPropagation()}> + {description ? ( +
- - - - } - /> - - - - + +
+ + + {description} + + +
+
+ ) : ( - + no description available - 0 ? ( +
- - -
-
- - {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) - )} - -
- + +
+ + + + + {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) + )} + + + + +
+
+ ) : null} +
); };