X-Git-Url: https://git.arvados.org/arvados-workbench2.git/blobdiff_plain/134cf300692c9f09f1a79d02295e1d6b7242f32d..63a5dc75ae9b68b3570e3bee1662155572cf5d1f:/src/views-components/data-explorer/renderers.tsx diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index aa200942..cbe815c0 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -3,17 +3,40 @@ // SPDX-License-Identifier: AGPL-3.0 import React from 'react'; -import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core'; +import { + Grid, + Typography, + withStyles, + Tooltip, + IconButton, + Checkbox, + Chip +} from '@material-ui/core'; import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star'; import { Resource, ResourceKind, TrashableResource } from 'models/resource'; -import { ProjectIcon, FilterGroupIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon, RemoveIcon, RenameIcon } from 'components/icon/icon'; +import { + FreezeIcon, + ProjectIcon, + FilterGroupIcon, + CollectionIcon, + ProcessIcon, + DefaultIcon, + ShareIcon, + CollectionOldVersionIcon, + WorkflowIcon, + RemoveIcon, + RenameIcon, + ActiveIcon, + SetupIcon, + InactiveIcon, +} from 'components/icon/icon'; import { formatDate, formatFileSize, formatTime } from 'common/formatters'; import { resourceLabel } from 'common/labels'; import { connect, DispatchProp } from 'react-redux'; import { RootState } from 'store/store'; -import { getResource } from 'store/resources/resources'; +import { getResource, filterResources } from 'store/resources/resources'; import { GroupContentsResource } from 'services/groups-service/groups-service'; -import { getProcess, Process, getProcessStatus, getProcessStatusColor, getProcessRuntime } from 'store/processes/process'; +import { getProcess, Process, getProcessStatus, getProcessStatusStyles, getProcessRuntime } from 'store/processes/process'; import { ArvadosTheme } from 'common/custom-theme'; import { compose, Dispatch } from 'redux'; import { WorkflowResource } from 'models/workflow'; @@ -21,17 +44,23 @@ import { ResourceStatus as WorkflowStatus } from 'views/workflow-panel/workflow- import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions'; import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions'; import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user'; -import { toggleIsActive, toggleIsAdmin } from 'store/users/users-actions'; -import { LinkResource } from 'models/link'; -import { navigateTo, navigateToGroupDetails } from 'store/navigation/navigation-action'; +import { toggleIsAdmin } from 'store/users/users-actions'; +import { LinkClass, LinkResource } from 'models/link'; +import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action'; import { withResourceData } from 'views-components/data-explorer/with-resources'; import { CollectionResource } from 'models/collection'; import { IllegalNamingWarning } from 'components/warning/warning'; import { loadResource } from 'store/resources/resources-actions'; -import { GroupClass } from 'models/group'; -import { openRemoveGroupMemberDialog, openEditPermissionLevelDialog } from 'store/group-details-panel/group-details-panel-actions'; +import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group'; +import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions'; +import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions'; import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select'; import { PermissionLevel } from 'models/permission'; +import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions'; +import { getUserUuid } from 'common/getuser'; +import { VirtualMachinesResource } from 'models/virtual-machines'; +import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar'; +import { ProjectResource } from 'models/project'; const renderName = (dispatch: Dispatch, item: GroupContentsResource) => { @@ -52,11 +81,32 @@ const renderName = (dispatch: Dispatch, item: GroupContentsResource) => { + { + item.kind === ResourceKind.PROJECT && + } ; }; +const FrozenProject = (props: {item: ProjectResource}) => { + const [fullUsername, setFullusername] = React.useState(null); + const getFullName = React.useCallback(() => { + if (props.item.frozenByUuid) { + setFullusername(); + } + }, [props.item, setFullusername]) + + if (props.item.frozenByUuid) { + + return Project was frozen by {fullUsername}}> + + ; + } else { + return null; + } +} + export const ResourceName = connect( (state: RootState, props: { uuid: string }) => { const resource = getResource(props.uuid)(state.resources); @@ -157,24 +207,32 @@ export const ResourceLastName = connect( return resource || { lastName: '' }; })(renderLastName); -const renderFullName = (item: { firstName: string, lastName: string }) => - {(item.firstName + " " + item.lastName).trim()}; +const renderFullName = (dispatch: Dispatch, item: { uuid: string, firstName: string, lastName: string }, link?: boolean) => { + const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid; + return link ? dispatch(navigateToUserProfile(item.uuid))}> + {displayName} + : + {displayName}; +} -export const ResourceFullName = connect( - (state: RootState, props: { uuid: string }) => { +export const UserResourceFullName = connect( + (state: RootState, props: { uuid: string, link?: boolean }) => { const resource = getResource(props.uuid)(state.resources); - return resource || { firstName: '', lastName: '' }; - })(renderFullName); - + return { item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link }; + })((props: { item: { uuid: string, firstName: string, lastName: string }, link?: boolean } & DispatchProp) => renderFullName(props.dispatch, props.item, props.link)); const renderUuid = (item: { uuid: string }) => - {item.uuid}; + + {item.uuid} + + ; -export const ResourceUuid = connect( - (state: RootState, props: { uuid: string }) => { - const resource = getResource(props.uuid)(state.resources); - return resource || { uuid: '' }; - })(renderUuid); +export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => ( + getResource(props.uuid)(state.resources) || { uuid: '' } +))(renderUuid); const renderEmail = (item: { email: string }) => {item.email}; @@ -185,38 +243,115 @@ export const ResourceEmail = connect( return resource || { email: '' }; })(renderEmail); -const renderIsActive = (props: { uuid: string, kind: ResourceKind, isActive: boolean, toggleIsActive: (uuid: string) => void }) => { - if (props.kind === ResourceKind.USER) { +enum UserAccountStatus { + ACTIVE = 'Active', + INACTIVE = 'Inactive', + SETUP = 'Setup', + UNKNOWN = '' +} + +const renderAccountStatus = (props: { status: UserAccountStatus }) => + + + {(() => { + switch (props.status) { + case UserAccountStatus.ACTIVE: + return ; + case UserAccountStatus.SETUP: + return ; + case UserAccountStatus.INACTIVE: + return ; + default: + return <>; + } + })()} + + + + {props.status} + + + ; + +const getUserAccountStatus = (state: RootState, props: { uuid: string }) => { + const user = getResource(props.uuid)(state.resources); + // Get membership links for all users group + const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL); + const permissions = filterResources((resource: LinkResource) => + resource.kind === ResourceKind.LINK && + resource.linkClass === LinkClass.PERMISSION && + resource.headUuid === allUsersGroupUuid && + resource.tailUuid === props.uuid + )(state.resources); + + if (user) { + return user.isActive ? { status: UserAccountStatus.ACTIVE } : permissions.length > 0 ? { status: UserAccountStatus.SETUP } : { status: UserAccountStatus.INACTIVE }; + } else { + return { status: UserAccountStatus.UNKNOWN }; + } +} + +export const ResourceLinkTailAccountStatus = connect( + (state: RootState, props: { uuid: string }) => { + const link = getResource(props.uuid)(state.resources); + return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN }; + })(renderAccountStatus); + +export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus); + +const renderIsHidden = (props: { + memberLinkUuid: string, + permissionLinkUuid: string, + visible: boolean, + canManage: boolean, + setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void +}) => { + if (props.memberLinkUuid) { return props.toggleIsActive(props.uuid)} />; + checked={props.visible} + disabled={!props.canManage} + onClick={(e) => { + e.stopPropagation(); + props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible); + }} />; } else { return ; } } -export const ResourceIsActive = connect( - (state: RootState, props: { uuid: string }) => { - const resource = getResource(props.uuid)(state.resources); - return resource || { isActive: false, kind: ResourceKind.NONE }; - }, { toggleIsActive } -)(renderIsActive); - -export const ResourceLinkTailIsActive = connect( +export const ResourceLinkTailIsVisible = connect( (state: RootState, props: { uuid: string }) => { const link = getResource(props.uuid)(state.resources); - const tailResource = getResource(link?.tailUuid || '')(state.resources); - - return tailResource || { isActive: false, kind: ResourceKind.NONE }; - }, { toggleIsActive } -)(renderIsActive); + const member = getResource(link?.tailUuid || '')(state.resources); + const group = getResource(link?.headUuid || '')(state.resources); + const permissions = filterResources((resource: LinkResource) => { + return resource.linkClass === LinkClass.PERMISSION + && resource.headUuid === link?.tailUuid + && resource.tailUuid === group?.uuid + && resource.name === PermissionLevel.CAN_READ; + })(state.resources); + + const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : ''; + const isVisible = link && group && permissions.length > 0; + // Consider whether the current user canManage this resurce in addition when it's possible + const isBuiltin = isBuiltinGroup(link?.headUuid || ''); + + return member?.kind === ResourceKind.USER + ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin } + : { memberLinkUuid: '', permissionLinkUuid: '', visible: false, canManage: false }; + }, { setMemberIsHidden } +)(renderIsHidden); const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (uuid: string) => void }) => props.toggleIsAdmin(props.uuid)} />; + onClick={(e) => { + e.stopPropagation(); + props.toggleIsAdmin(props.uuid); + }} />; export const ResourceIsAdmin = connect( (state: RootState, props: { uuid: string }) => { @@ -225,15 +360,37 @@ export const ResourceIsAdmin = connect( }, { toggleIsAdmin } )(renderIsAdmin); -const renderUsername = (item: { username: string }) => - {item.username}; +const renderUsername = (item: { username: string, uuid: string }) => + {item.username || item.uuid}; export const ResourceUsername = connect( (state: RootState, props: { uuid: string }) => { const resource = getResource(props.uuid)(state.resources); - return resource || { username: '' }; + return resource || { username: '', uuid: props.uuid }; })(renderUsername); +// Virtual machine resource + +const renderHostname = (item: { hostname: string }) => + {item.hostname}; + +export const VirtualMachineHostname = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { hostname: '' }; + })(renderHostname); + +const renderVirtualMachineLogin = (login: { user: string }) => + {login.user} + +export const VirtualMachineLogin = connect( + (state: RootState, props: { linkUuid: string }) => { + const permission = getResource(props.linkUuid)(state.resources); + const user = getResource(permission?.tailUuid || '')(state.resources); + + return { user: user?.username || permission?.tailUuid || '' }; + })(renderVirtualMachineLogin); + // Common methods const renderCommonData = (data: string) => {data}; @@ -273,7 +430,7 @@ const clusterColors = [ export const ResourceCluster = (props: { uuid: string }) => { const CLUSTER_ID_LENGTH = 5; const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5; - const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substr(0, pos) : ''; + const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : ''; const ci = pos >= CLUSTER_ID_LENGTH ? ((((( (props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1)) + props.uuid.charCodeAt(2)) @@ -308,7 +465,7 @@ export const ResourceLinkClass = connect( const getResourceDisplayName = (resource: Resource): string => { if ((resource as UserResource).kind === ResourceKind.USER - && typeof (resource as UserResource).firstName !== 'undefined') { + && typeof (resource as UserResource).firstName !== 'undefined') { // We can be sure the resource is UserResource return getUserDisplayName(resource as UserResource); } else { @@ -320,15 +477,7 @@ const renderResourceLink = (dispatch: Dispatch, item: Resource) => { var displayName = getResourceDisplayName(item); return dispatch(navigateTo(item.uuid))}> - {resourceLabel(item.kind)}: {displayName || item.uuid} - ; -}; - -const renderResource = (dispatch: Dispatch, item: Resource) => { - var displayName = getResourceDisplayName(item); - - return - {resourceLabel(item.kind)}: {displayName || item.uuid} + {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || '' : '')}: {displayName || item.uuid} ; }; @@ -376,26 +525,35 @@ export const ResourceLinkTailUuid = connect( return tailResource || { uuid: '' }; })(renderUuid); -const renderLinkDelete = (dispatch: Dispatch, item: LinkResource) => { +const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => { if (item.uuid) { - return - dispatch(openRemoveGroupMemberDialog(item.uuid))}> - - - ; + return canManage ? + + dispatch(openRemoveGroupMemberDialog(item.uuid))}> + + + : + + + + + ; } else { - return ; + return ; } } export const ResourceLinkDelete = connect( (state: RootState, props: { uuid: string }) => { const link = getResource(props.uuid)(state.resources); + const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || ''); + return { - item: link || { uuid: '', kind: ResourceKind.NONE } + item: link || { uuid: '', kind: ResourceKind.NONE }, + canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin, }; - })((props: { item: LinkResource } & DispatchProp) => - renderLinkDelete(props.dispatch, props.item)); + })((props: { item: LinkResource, canManage: boolean } & DispatchProp) => + renderLinkDelete(props.dispatch, props.item, props.canManage)); export const ResourceLinkTailEmail = connect( (state: RootState, props: { uuid: string }) => { @@ -413,48 +571,54 @@ export const ResourceLinkTailUsername = connect( return resource || { username: '' }; })(renderUsername); -const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, resource: Resource) => { +const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => { return {formatPermissionLevel(link.name as PermissionLevel)} - dispatch(openEditPermissionLevelDialog(link.uuid, resource.uuid))}> - - + {canManage ? + dispatch(openPermissionEditContextMenu(event, link))}> + + : + '' + } ; } export const ResourceLinkHeadPermissionLevel = connect( (state: RootState, props: { uuid: string }) => { const link = getResource(props.uuid)(state.resources); - const resource = getResource(link?.headUuid || '')(state.resources); + const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || ''); return { link: link || { uuid: '', name: '', kind: ResourceKind.NONE }, - resource: resource || { uuid: '', kind: ResourceKind.NONE } + canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin, }; - })((props: { link: LinkResource, resource: Resource } & DispatchProp) => - renderPermissionLevel(props.dispatch, props.link, props.resource)); + })((props: { link: LinkResource, canManage: boolean } & DispatchProp) => + renderPermissionLevel(props.dispatch, props.link, props.canManage)); export const ResourceLinkTailPermissionLevel = connect( (state: RootState, props: { uuid: string }) => { const link = getResource(props.uuid)(state.resources); - const resource = getResource(link?.tailUuid || '')(state.resources); + const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || ''); return { link: link || { uuid: '', name: '', kind: ResourceKind.NONE }, - resource: resource || { uuid: '', kind: ResourceKind.NONE } + canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin, }; - })((props: { link: LinkResource, resource: Resource } & DispatchProp) => - renderPermissionLevel(props.dispatch, props.link, props.resource)); + })((props: { link: LinkResource, canManage: boolean } & DispatchProp) => + renderPermissionLevel(props.dispatch, props.link, props.canManage)); -// Displays resource type and display name without link -export const ResourceLabel = connect( - (state: RootState, props: { uuid: string }) => { - const resource = getResource(props.uuid)(state.resources); - return { - item: resource || { uuid: '', kind: ResourceKind.NONE } - }; - })((props: { item: Resource } & DispatchProp) => - renderResource(props.dispatch, props.item)); +const getResourceLinkCanManage = (state: RootState, link: LinkResource) => { + const headResource = getResource(link.headUuid)(state.resources); + // const tailResource = getResource(link.tailUuid)(state.resources); + const userUuid = getUserUuid(state); + + if (headResource && headResource.kind === ResourceKind.GROUP) { + return userUuid ? (headResource as GroupResource).writableBy?.includes(userUuid) : false; + } else { + // true for now + return true; + } +} // Process Resources const resourceRunProcess = (dispatch: Dispatch, uuid: string) => { @@ -572,10 +736,17 @@ const userFromID = return { uuid: props.uuid, userFullname }; }); -export const ResourceOwnerWithName = +const ownerFromResourceId = compose( - userFromID, - withStyles({}, { withTheme: true })) + connect((state: RootState, props: { uuid: string }) => { + const childResource = getResource(props.uuid)(state.resources); + return { uuid: childResource ? (childResource as Resource).ownerUuid : '' }; + }), + userFromID + ); + +const _resourceWithName = + withStyles({}, { withTheme: true }) ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => { const { uuid, userFullname, dispatch, theme } = props; @@ -591,9 +762,13 @@ export const ResourceOwnerWithName = ; }); +export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName); + +export const ResourceWithName = userFromID(_resourceWithName); + export const UserNameFromID = compose(userFromID)( - (props: { uuid: string, userFullname: string, dispatch: Dispatch }) => { + (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => { const { uuid, userFullname, dispatch } = props; if (userFullname === '') { @@ -657,7 +832,7 @@ export const ResponsiblePerson = return {responsiblePersonName} ({uuid}) - ; + ; }); const renderType = (type: string, subtype: string) => @@ -687,19 +862,35 @@ export const CollectionStatus = connect((state: RootState, props: { uuid: string : head version ); +export const CollectionName = connect((state: RootState, props: { uuid: string, className?: string }) => { + return { + collection: getResource(props.uuid)(state.resources), + uuid: props.uuid, + className: props.className, + }; +})((props: { collection: CollectionResource, uuid: string, className?: string }) => + {props.collection?.name || props.uuid} +); + export const ProcessStatus = compose( connect((state: RootState, props: { uuid: string }) => { return { process: getProcess(props.uuid)(state.resources) }; }), withStyles({}, { withTheme: true })) - ((props: { process?: Process, theme: ArvadosTheme }) => { - const status = props.process ? getProcessStatus(props.process) : "-"; - return - {status} - ; - }); + ((props: { process?: Process, theme: ArvadosTheme }) => + props.process + ? + : - + ); export const ProcessStartDate = connect( (state: RootState, props: { uuid: string }) => {