1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
6 import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Chip } from '@material-ui/core';
7 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
8 import { Resource, ResourceKind, TrashableResource } from 'models/resource';
17 CollectionOldVersionIcon,
24 } from 'components/icon/icon';
25 import { formatDate, formatFileSize, formatTime } from 'common/formatters';
26 import { resourceLabel } from 'common/labels';
27 import { connect, DispatchProp } from 'react-redux';
28 import { RootState } from 'store/store';
29 import { getResource, filterResources } from 'store/resources/resources';
30 import { GroupContentsResource } from 'services/groups-service/groups-service';
31 import { getProcess, Process, getProcessStatus, getProcessStatusStyles, getProcessRuntime } from 'store/processes/process';
32 import { ArvadosTheme } from 'common/custom-theme';
33 import { compose, Dispatch } from 'redux';
34 import { WorkflowResource } from 'models/workflow';
35 import { ResourceStatus as WorkflowStatus } from 'views/workflow-panel/workflow-panel-view';
36 import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
37 import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
38 import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
39 import { toggleIsAdmin } from 'store/users/users-actions';
40 import { LinkClass, LinkResource } from 'models/link';
41 import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action';
42 import { withResourceData } from 'views-components/data-explorer/with-resources';
43 import { CollectionResource } from 'models/collection';
44 import { IllegalNamingWarning } from 'components/warning/warning';
45 import { loadResource } from 'store/resources/resources-actions';
46 import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
47 import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
48 import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions';
49 import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
50 import { PermissionLevel } from 'models/permission';
51 import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
52 import { getUserUuid } from 'common/getuser';
53 import { VirtualMachinesResource } from 'models/virtual-machines';
54 import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
55 import { ProjectResource } from 'models/project';
56 import { ProcessResource } from 'models/process';
58 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
59 const navFunc = 'groupClass' in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo;
61 <Grid container alignItems='center' wrap='nowrap' spacing={16}>
62 <Grid item>{renderIcon(item)}</Grid>
64 <Typography color='primary' style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navFunc(item.uuid))}>
65 {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION ? <IllegalNamingWarning name={item.name} /> : null}
70 <Typography variant='caption'>
71 <FavoriteStar resourceUuid={item.uuid} />
72 <PublicFavoriteStar resourceUuid={item.uuid} />
73 {item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />}
80 const FrozenProject = (props: { item: ProjectResource }) => {
81 const [fullUsername, setFullusername] = React.useState<any>(null);
82 const getFullName = React.useCallback(() => {
83 if (props.item.frozenByUuid) {
84 setFullusername(<UserNameFromID uuid={props.item.frozenByUuid} />);
86 }, [props.item, setFullusername]);
88 if (props.item.frozenByUuid) {
90 <Tooltip onOpen={getFullName} enterDelay={500} title={<span>Project was frozen by {fullUsername}</span>}>
91 <FreezeIcon style={{ fontSize: 'inherit' }} />
99 export const ResourceName = connect((state: RootState, props: { uuid: string }) => {
100 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
102 })((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
104 const renderIcon = (item: GroupContentsResource) => {
106 case ResourceKind.PROJECT:
107 if (item.groupClass === GroupClass.FILTER) {
108 return <FilterGroupIcon />;
110 return <ProjectIcon />;
111 case ResourceKind.COLLECTION:
112 if (item.uuid === item.currentVersionUuid) {
113 return <CollectionIcon />;
115 return <CollectionOldVersionIcon />;
116 case ResourceKind.PROCESS:
117 return <ProcessIcon />;
118 case ResourceKind.WORKFLOW:
119 return <WorkflowIcon />;
121 return <DefaultIcon />;
125 const renderDate = (date?: string) => {
127 <Typography noWrap style={{ minWidth: '100px' }}>
133 const renderWorkflowName = (item: WorkflowResource) => (
134 <Grid container alignItems='center' wrap='nowrap' spacing={16}>
135 <Grid item>{renderIcon(item)}</Grid>
137 <Typography color='primary' style={{ width: '100px' }}>
144 export const ResourceWorkflowName = connect((state: RootState, props: { uuid: string }) => {
145 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
147 })(renderWorkflowName);
149 const getPublicUuid = (uuidPrefix: string) => {
150 return `${uuidPrefix}-tpzed-anonymouspublic`;
153 const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
154 const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
157 {!isPublic && uuid && (
158 <Tooltip title='Share'>
159 <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
168 export const ResourceShare = connect((state: RootState, props: { uuid: string }) => {
169 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
170 const uuidPrefix = getUuidPrefix(state);
172 uuid: resource ? resource.uuid : '',
173 ownerUuid: resource ? resource.ownerUuid : '',
176 })((props: { ownerUuid?: string; uuidPrefix: string; uuid?: string } & DispatchProp<any>) =>
177 resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid)
181 const renderFirstName = (item: { firstName: string }) => {
182 return <Typography noWrap>{item.firstName}</Typography>;
185 export const ResourceFirstName = connect((state: RootState, props: { uuid: string }) => {
186 const resource = getResource<UserResource>(props.uuid)(state.resources);
187 return resource || { firstName: '' };
190 const renderLastName = (item: { lastName: string }) => <Typography noWrap>{item.lastName}</Typography>;
192 export const ResourceLastName = connect((state: RootState, props: { uuid: string }) => {
193 const resource = getResource<UserResource>(props.uuid)(state.resources);
194 return resource || { lastName: '' };
197 const renderFullName = (dispatch: Dispatch, item: { uuid: string; firstName: string; lastName: string }, link?: boolean) => {
198 const displayName = (item.firstName + ' ' + item.lastName).trim() || item.uuid;
200 <Typography noWrap color='primary' style={{ cursor: 'pointer' }} onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
204 <Typography noWrap>{displayName}</Typography>
208 export const UserResourceFullName = connect((state: RootState, props: { uuid: string; link?: boolean }) => {
209 const resource = getResource<UserResource>(props.uuid)(state.resources);
210 return { item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link };
211 })((props: { item: { uuid: string; firstName: string; lastName: string }; link?: boolean } & DispatchProp<any>) =>
212 renderFullName(props.dispatch, props.item, props.link)
215 const renderUuid = (item: { uuid: string }) => (
216 <Typography data-cy='uuid' noWrap>
218 {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-'}
222 const renderUuidCopyIcon = (item: { uuid: string }) => (
223 <Typography data-cy='uuid' noWrap>
224 {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-'}
228 export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' })(renderUuid);
230 const renderEmail = (item: { email: string }) => <Typography noWrap>{item.email}</Typography>;
232 export const ResourceEmail = connect((state: RootState, props: { uuid: string }) => {
233 const resource = getResource<UserResource>(props.uuid)(state.resources);
234 return resource || { email: '' };
237 enum UserAccountStatus {
239 INACTIVE = 'Inactive',
244 const renderAccountStatus = (props: { status: UserAccountStatus }) => (
245 <Grid container alignItems='center' wrap='nowrap' spacing={8} data-cy='account-status'>
248 switch (props.status) {
249 case UserAccountStatus.ACTIVE:
250 return <ActiveIcon style={{ color: '#4caf50', verticalAlign: 'middle' }} />;
251 case UserAccountStatus.SETUP:
252 return <SetupIcon style={{ color: '#2196f3', verticalAlign: 'middle' }} />;
253 case UserAccountStatus.INACTIVE:
254 return <InactiveIcon style={{ color: '#9e9e9e', verticalAlign: 'middle' }} />;
261 <Typography noWrap>{props.status}</Typography>
266 const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
267 const user = getResource<UserResource>(props.uuid)(state.resources);
268 // Get membership links for all users group
269 const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
270 const permissions = filterResources(
271 (resource: LinkResource) =>
272 resource.kind === ResourceKind.LINK &&
273 resource.linkClass === LinkClass.PERMISSION &&
274 resource.headUuid === allUsersGroupUuid &&
275 resource.tailUuid === props.uuid
280 ? { status: UserAccountStatus.ACTIVE }
281 : permissions.length > 0
282 ? { status: UserAccountStatus.SETUP }
283 : { status: UserAccountStatus.INACTIVE };
285 return { status: UserAccountStatus.UNKNOWN };
289 export const ResourceLinkTailAccountStatus = connect((state: RootState, props: { uuid: string }) => {
290 const link = getResource<LinkResource>(props.uuid)(state.resources);
291 return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN };
292 })(renderAccountStatus);
294 export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
296 const renderIsHidden = (props: {
297 memberLinkUuid: string;
298 permissionLinkUuid: string;
301 setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void;
303 if (props.memberLinkUuid) {
306 data-cy='user-visible-checkbox'
308 checked={props.visible}
309 disabled={!props.canManage}
312 props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
317 return <Typography />;
321 export const ResourceLinkTailIsVisible = connect(
322 (state: RootState, props: { uuid: string }) => {
323 const link = getResource<LinkResource>(props.uuid)(state.resources);
324 const member = getResource<Resource>(link?.tailUuid || '')(state.resources);
325 const group = getResource<GroupResource>(link?.headUuid || '')(state.resources);
326 const permissions = filterResources((resource: LinkResource) => {
328 resource.linkClass === LinkClass.PERMISSION &&
329 resource.headUuid === link?.tailUuid &&
330 resource.tailUuid === group?.uuid &&
331 resource.name === PermissionLevel.CAN_READ
335 const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : '';
336 const isVisible = link && group && permissions.length > 0;
337 // Consider whether the current user canManage this resurce in addition when it's possible
338 const isBuiltin = isBuiltinGroup(link?.headUuid || '');
340 return member?.kind === ResourceKind.USER
341 ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
342 : { memberLinkUuid: '', permissionLinkUuid: '', visible: false, canManage: false };
344 { setMemberIsHidden }
347 const renderIsAdmin = (props: { uuid: string; isAdmin: boolean; toggleIsAdmin: (uuid: string) => void }) => (
350 checked={props.isAdmin}
353 props.toggleIsAdmin(props.uuid);
358 export const ResourceIsAdmin = connect(
359 (state: RootState, props: { uuid: string }) => {
360 const resource = getResource<UserResource>(props.uuid)(state.resources);
361 return resource || { isAdmin: false };
366 const renderUsername = (item: { username: string; uuid: string }) => <Typography noWrap>{item.username || item.uuid}</Typography>;
368 export const ResourceUsername = connect((state: RootState, props: { uuid: string }) => {
369 const resource = getResource<UserResource>(props.uuid)(state.resources);
370 return resource || { username: '', uuid: props.uuid };
373 // Virtual machine resource
375 const renderHostname = (item: { hostname: string }) => <Typography noWrap>{item.hostname}</Typography>;
377 export const VirtualMachineHostname = connect((state: RootState, props: { uuid: string }) => {
378 const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
379 return resource || { hostname: '' };
382 const renderVirtualMachineLogin = (login: { user: string }) => <Typography noWrap>{login.user}</Typography>;
384 export const VirtualMachineLogin = connect((state: RootState, props: { linkUuid: string }) => {
385 const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
386 const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
388 return { user: user?.username || permission?.tailUuid || '' };
389 })(renderVirtualMachineLogin);
392 const renderCommonData = (data: string) => <Typography noWrap>{data}</Typography>;
394 const renderCommonDate = (date: string) => <Typography noWrap>{formatDate(date)}</Typography>;
396 export const CommonUuid = withResourceData('uuid', renderCommonData);
398 // Api Client Authorizations
399 export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
401 export const TokenApiToken = withResourceData('apiToken', renderCommonData);
403 export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
405 export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
407 export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
409 export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
411 export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
413 export const TokenScopes = withResourceData('scopes', renderCommonData);
415 export const TokenUserId = withResourceData('userId', renderCommonData);
417 const clusterColors = [
425 export const ResourceCluster = (props: { uuid: string }) => {
426 const CLUSTER_ID_LENGTH = 5;
427 const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
428 const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : '';
430 pos >= CLUSTER_ID_LENGTH
431 ? ((props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1) + props.uuid.charCodeAt(2)) * props.uuid.charCodeAt(3) + props.uuid.charCodeAt(4)) %
437 backgroundColor: clusterColors[ci][0],
438 color: clusterColors[ci][1],
449 const renderLinkName = (item: { name: string }) => <Typography noWrap>{item.name || '-'}</Typography>;
451 export const ResourceLinkName = connect((state: RootState, props: { uuid: string }) => {
452 const resource = getResource<LinkResource>(props.uuid)(state.resources);
453 return resource || { name: '' };
456 const renderLinkClass = (item: { linkClass: string }) => <Typography noWrap>{item.linkClass}</Typography>;
458 export const ResourceLinkClass = connect((state: RootState, props: { uuid: string }) => {
459 const resource = getResource<LinkResource>(props.uuid)(state.resources);
460 return resource || { linkClass: '' };
463 const getResourceDisplayName = (resource: Resource): string => {
464 if ((resource as UserResource).kind === ResourceKind.USER && typeof (resource as UserResource).firstName !== 'undefined') {
465 // We can be sure the resource is UserResource
466 return getUserDisplayName(resource as UserResource);
468 return (resource as GroupContentsResource).name;
472 const renderResourceLink = (dispatch: Dispatch, item: Resource) => {
473 var displayName = getResourceDisplayName(item);
476 <Typography noWrap color='primary' style={{ cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
477 {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || '' : '')}: {displayName || item.uuid}
482 export const ResourceLinkTail = connect((state: RootState, props: { uuid: string }) => {
483 const resource = getResource<LinkResource>(props.uuid)(state.resources);
484 const tailResource = getResource<Resource>(resource?.tailUuid || '')(state.resources);
487 item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.tailKind || ResourceKind.NONE },
489 })((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
491 export const ResourceLinkHead = connect((state: RootState, props: { uuid: string }) => {
492 const resource = getResource<LinkResource>(props.uuid)(state.resources);
493 const headResource = getResource<Resource>(resource?.headUuid || '')(state.resources);
496 item: headResource || { uuid: resource?.headUuid || '', kind: resource?.headKind || ResourceKind.NONE },
498 })((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
500 export const ResourceLinkUuid = connect((state: RootState, props: { uuid: string }) => {
501 const resource = getResource<LinkResource>(props.uuid)(state.resources);
502 return resource || { uuid: '' };
505 export const ResourceLinkHeadUuid = connect((state: RootState, props: { uuid: string }) => {
506 const link = getResource<LinkResource>(props.uuid)(state.resources);
507 const headResource = getResource<Resource>(link?.headUuid || '')(state.resources);
509 return headResource || { uuid: '' };
512 export const ResourceLinkTailUuid = connect((state: RootState, props: { uuid: string }) => {
513 const link = getResource<LinkResource>(props.uuid)(state.resources);
514 const tailResource = getResource<Resource>(link?.tailUuid || '')(state.resources);
516 return tailResource || { uuid: '' };
519 const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => {
523 <IconButton data-cy='resource-delete-button' onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}>
529 <IconButton disabled data-cy='resource-delete-button'>
535 return <Typography noWrap></Typography>;
539 export const ResourceLinkDelete = connect((state: RootState, props: { uuid: string }) => {
540 const link = getResource<LinkResource>(props.uuid)(state.resources);
541 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
544 item: link || { uuid: '', kind: ResourceKind.NONE },
545 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
547 })((props: { item: LinkResource; canManage: boolean } & DispatchProp<any>) => renderLinkDelete(props.dispatch, props.item, props.canManage));
549 export const ResourceLinkTailEmail = connect((state: RootState, props: { uuid: string }) => {
550 const link = getResource<LinkResource>(props.uuid)(state.resources);
551 const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
553 return resource || { email: '' };
556 export const ResourceLinkTailUsername = connect((state: RootState, props: { uuid: string }) => {
557 const link = getResource<LinkResource>(props.uuid)(state.resources);
558 const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
560 return resource || { username: '' };
563 const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
566 {formatPermissionLevel(link.name as PermissionLevel)}
568 <IconButton data-cy='edit-permission-button' onClick={(event) => dispatch<any>(openPermissionEditContextMenu(event, link))}>
578 export const ResourceLinkHeadPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
579 const link = getResource<LinkResource>(props.uuid)(state.resources);
580 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
583 link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
584 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
586 })((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
588 export const ResourceLinkTailPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
589 const link = getResource<LinkResource>(props.uuid)(state.resources);
590 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
593 link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
594 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
596 })((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
598 const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
599 const headResource = getResource<Resource>(link.headUuid)(state.resources);
600 // const tailResource = getResource<Resource>(link.tailUuid)(state.resources);
601 const userUuid = getUserUuid(state);
603 if (headResource && headResource.kind === ResourceKind.GROUP) {
604 return userUuid ? (headResource as GroupResource).writableBy?.includes(userUuid) : false;
612 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
616 <Tooltip title='Run process'>
617 <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
626 export const ResourceRunProcess = connect((state: RootState, props: { uuid: string }) => {
627 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
629 uuid: resource ? resource.uuid : '',
631 })((props: { uuid: string } & DispatchProp<any>) => resourceRunProcess(props.dispatch, props.uuid));
633 const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
634 if (ownerUuid === getPublicUuid(uuidPrefix)) {
635 return renderStatus(WorkflowStatus.PUBLIC);
637 return renderStatus(WorkflowStatus.PRIVATE);
641 const renderStatus = (status: string) => (
642 <Typography noWrap style={{ width: '60px' }}>
647 export const ResourceWorkflowStatus = connect((state: RootState, props: { uuid: string }) => {
648 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
649 const uuidPrefix = getUuidPrefix(state);
651 ownerUuid: resource ? resource.ownerUuid : '',
654 })((props: { ownerUuid?: string; uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
656 export const ResourceContainerUuid = connect((state: RootState, props: { uuid: string }) => {
657 const process = getProcess(props.uuid)(state.resources);
658 return { uuid: process?.container?.uuid ? process?.container?.uuid : '' };
659 })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
661 enum ColumnSelection {
662 OUTPUT_UUID = 'outputUuid',
663 LOG_UUID = 'logUuid',
666 const renderUuidLinkWithCopyIcon = (dispatch: Dispatch, item: ProcessResource, column: string) => {
667 const selectedColumnUuid = item[column];
669 <Grid container alignItems='center' wrap='nowrap'>
671 {selectedColumnUuid ? (
672 <Typography color='primary' style={{ width: 'auto', cursor: 'pointer' }} noWrap onClick={() => dispatch<any>(navigateTo(selectedColumnUuid))}>
679 <Grid item>{selectedColumnUuid && renderUuidCopyIcon({ uuid: selectedColumnUuid })}</Grid>
684 export const ResourceOutputUuid = connect((state: RootState, props: { uuid: string }) => {
685 const resource = getResource<ProcessResource>(props.uuid)(state.resources);
687 })((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.OUTPUT_UUID));
689 export const ResourceLogUuid = connect((state: RootState, props: { uuid: string }) => {
690 const resource = getResource<ProcessResource>(props.uuid)(state.resources);
692 })((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.LOG_UUID));
694 export const ResourceParentProcess = connect((state: RootState, props: { uuid: string }) => {
695 const process = getProcess(props.uuid)(state.resources);
696 return { parentProcess: process?.containerRequest?.requestingContainerUuid || '' };
697 })((props: { parentProcess: string }) => renderUuid({ uuid: props.parentProcess }));
699 export const ResourceModifiedByUserUuid = connect((state: RootState, props: { uuid: string }) => {
700 const process = getProcess(props.uuid)(state.resources);
701 return { userUuid: process?.containerRequest?.modifiedByUserUuid || '' };
702 })((props: { userUuid: string }) => renderUuid({ uuid: props.userUuid }));
704 export const ResourceCreatedAtDate = connect((state: RootState, props: { uuid: string }) => {
705 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
706 return { date: resource ? resource.createdAt : '' };
707 })((props: { date: string }) => renderDate(props.date));
709 export const ResourceLastModifiedDate = connect((state: RootState, props: { uuid: string }) => {
710 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
711 return { date: resource ? resource.modifiedAt : '' };
712 })((props: { date: string }) => renderDate(props.date));
714 export const ResourceTrashDate = connect((state: RootState, props: { uuid: string }) => {
715 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
716 return { date: resource ? resource.trashAt : '' };
717 })((props: { date: string }) => renderDate(props.date));
719 export const ResourceDeleteDate = connect((state: RootState, props: { uuid: string }) => {
720 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
721 return { date: resource ? resource.deleteAt : '' };
722 })((props: { date: string }) => renderDate(props.date));
724 export const renderFileSize = (fileSize?: number) => (
725 <Typography noWrap style={{ minWidth: '45px' }}>
726 {formatFileSize(fileSize)}
730 export const ResourceFileSize = connect((state: RootState, props: { uuid: string }) => {
731 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
733 if (resource && resource.kind !== ResourceKind.COLLECTION) {
734 return { fileSize: '' };
737 return { fileSize: resource ? resource.fileSizeTotal : 0 };
738 })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
740 const renderOwner = (owner: string) => <Typography noWrap>{owner || '-'}</Typography>;
742 export const ResourceOwner = connect((state: RootState, props: { uuid: string }) => {
743 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
744 return { owner: resource ? resource.ownerUuid : '' };
745 })((props: { owner: string }) => renderOwner(props.owner));
747 export const ResourceOwnerName = connect((state: RootState, props: { uuid: string }) => {
748 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
749 const ownerNameState = state.ownerName;
750 const ownerName = ownerNameState.find((it) => it.uuid === resource!.ownerUuid);
751 return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
752 })((props: { owner: string }) => renderOwner(props.owner));
754 export const ResourceUUID = connect((state: RootState, props: { uuid: string }) => {
755 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
756 return { uuid: resource ? resource.uuid : '' };
757 })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
759 const renderVersion = (version: number) => {
760 return <Typography>{version ?? '-'}</Typography>;
763 export const ResourceVersion = connect((state: RootState, props: { uuid: string }) => {
764 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
765 return { version: resource ? resource.version : '' };
766 })((props: { version: number }) => renderVersion(props.version));
768 const renderPortableDataHash = (portableDataHash: string | null) => (
770 {portableDataHash ? (
773 <CopyToClipboardSnackbar value={portableDataHash} />
781 export const ResourcePortableDataHash = connect((state: RootState, props: { uuid: string }) => {
782 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
783 return { portableDataHash: resource ? resource.portableDataHash : '' };
784 })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
786 const renderFileCount = (fileCount: number) => {
787 return <Typography>{fileCount ?? '-'}</Typography>;
790 export const ResourceFileCount = connect((state: RootState, props: { uuid: string }) => {
791 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
792 return { fileCount: resource ? resource.fileCount : '' };
793 })((props: { fileCount: number }) => renderFileCount(props.fileCount));
795 const userFromID = connect((state: RootState, props: { uuid: string }) => {
796 let userFullname = '';
797 const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
800 userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
803 return { uuid: props.uuid, userFullname };
806 const ownerFromResourceId = compose(
807 connect((state: RootState, props: { uuid: string }) => {
808 const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
809 return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
814 const _resourceWithName = withStyles(
817 )((props: { uuid: string; userFullname: string; dispatch: Dispatch; theme: ArvadosTheme }) => {
818 const { uuid, userFullname, dispatch, theme } = props;
819 if (userFullname === '') {
820 dispatch<any>(loadResource(uuid, false));
822 <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
829 <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
830 {userFullname} ({uuid})
835 export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
837 export const ResourceWithName = userFromID(_resourceWithName);
839 export const UserNameFromID = compose(userFromID)((props: { uuid: string; displayAsText?: string; userFullname: string; dispatch: Dispatch }) => {
840 const { uuid, userFullname, dispatch } = props;
842 if (userFullname === '') {
843 dispatch<any>(loadResource(uuid, false));
845 return <span>{userFullname ? userFullname : uuid}</span>;
848 export const ResponsiblePerson = compose(
849 connect((state: RootState, props: { uuid: string; parentRef: HTMLElement | null }) => {
850 let responsiblePersonName: string = '';
851 let responsiblePersonUUID: string = '';
852 let responsiblePersonProperty: string = '';
854 if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
856 const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
858 while (!responsiblePersonProperty && keys[index]) {
859 const key = keys[index];
860 if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
861 responsiblePersonProperty = key;
867 let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
869 while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
870 responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
871 resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
874 if (resource && resource.kind === ResourceKind.USER) {
875 responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
878 return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
880 withStyles({}, { withTheme: true })
881 )((props: { uuid: string | null; responsiblePersonName: string; parentRef: HTMLElement | null; theme: ArvadosTheme }) => {
882 const { uuid, responsiblePersonName, parentRef, theme } = props;
884 if (!uuid && parentRef) {
885 parentRef.style.display = 'none';
887 } else if (parentRef) {
888 parentRef.style.display = 'block';
891 if (!responsiblePersonName) {
893 <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
900 <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
901 {responsiblePersonName} ({uuid})
906 const renderType = (type: string, subtype: string) => <Typography noWrap>{resourceLabel(type, subtype)}</Typography>;
908 export const ResourceType = connect((state: RootState, props: { uuid: string }) => {
909 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
910 return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
911 })((props: { type: string; subtype: string }) => renderType(props.type, props.subtype));
913 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
914 return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
915 })((props: { resource: GroupContentsResource }) =>
916 props.resource && props.resource.kind === ResourceKind.COLLECTION ? <CollectionStatus uuid={props.resource.uuid} /> : <ProcessStatus uuid={props.resource.uuid} />
919 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
920 return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
921 })((props: { collection: CollectionResource }) =>
922 props.collection.uuid !== props.collection.currentVersionUuid ? <Typography>version {props.collection.version}</Typography> : <Typography>head version</Typography>
925 export const CollectionName = connect((state: RootState, props: { uuid: string; className?: string }) => {
927 collection: getResource<CollectionResource>(props.uuid)(state.resources),
929 className: props.className,
931 })((props: { collection: CollectionResource; uuid: string; className?: string }) => (
932 <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
935 export const ProcessStatus = compose(
936 connect((state: RootState, props: { uuid: string }) => {
937 return { process: getProcess(props.uuid)(state.resources) };
939 withStyles({}, { withTheme: true })
940 )((props: { process?: Process; theme: ArvadosTheme }) =>
943 label={getProcessStatus(props.process)}
945 height: props.theme.spacing.unit * 3,
946 width: props.theme.spacing.unit * 12,
947 ...getProcessStatusStyles(getProcessStatus(props.process), props.theme),
948 fontSize: '0.875rem',
949 borderRadius: props.theme.spacing.unit * 0.625,
953 <Typography>-</Typography>
957 export const ProcessStartDate = connect((state: RootState, props: { uuid: string }) => {
958 const process = getProcess(props.uuid)(state.resources);
959 return { date: process && process.container ? process.container.startedAt : '' };
960 })((props: { date: string }) => renderDate(props.date));
962 export const renderRunTime = (time: number) => (
963 <Typography noWrap style={{ minWidth: '45px' }}>
964 {formatTime(time, true)}
968 interface ContainerRunTimeProps {
972 interface ContainerRunTimeState {
976 export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
977 return { process: getProcess(props.uuid)(state.resources) };
979 class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
982 constructor(props: ContainerRunTimeProps) {
984 this.state = { runtime: this.getRuntime() };
988 return this.props.process ? getProcessRuntime(this.props.process) : 0;
992 this.setState({ runtime: this.getRuntime() });
995 componentDidMount() {
996 this.timer = setInterval(this.updateRuntime.bind(this), 5000);
999 componentWillUnmount() {
1000 clearInterval(this.timer);
1004 return this.props.process ? renderRunTime(this.state.runtime) : <Typography>-</Typography>;